Compare commits
232 Commits
css-module
...
sqlc-codeg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b8829bd58 | ||
|
|
14fe567687 | ||
|
|
daceb36e65 | ||
|
|
1c561d056c | ||
|
|
dd9d8d0579 | ||
|
|
d0cbe49c68 | ||
|
|
3e83533d06 | ||
|
|
eff3209a34 | ||
|
|
93aabef5d3 | ||
|
|
1a8883d604 | ||
|
|
0cf4be2fd2 | ||
|
|
615cfdb48b | ||
|
|
19989ab207 | ||
|
|
efb95e2e18 | ||
|
|
3ad97e619d | ||
|
|
5b7c89d8ee | ||
|
|
05d2f186a0 | ||
|
|
c8f77a0925 | ||
|
|
1626b31be7 | ||
|
|
2659b87941 | ||
|
|
b2c923e496 | ||
|
|
cc1fa36312 | ||
|
|
498dfc7435 | ||
|
|
6780dbbf52 | ||
|
|
86f64c3d9f | ||
|
|
ca17b89463 | ||
|
|
38b7b8c0b7 | ||
|
|
94e58da90d | ||
|
|
6a035e234c | ||
|
|
b75caae5f3 | ||
|
|
a5af036433 | ||
|
|
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 | ||
|
|
35c9ea0a29 | ||
|
|
2176f18597 | ||
|
|
af22e8cf22 | ||
|
|
d3cb526e49 | ||
|
|
6be1b2761e | ||
|
|
21b94190f6 | ||
|
|
72951848e5 | ||
|
|
364ec28c56 | ||
|
|
63e4a7ccb2 | ||
|
|
3fafdbd58a | ||
|
|
df8d8a2f41 | ||
|
|
7d07cc9ac8 | ||
|
|
cf03ebca8d | ||
|
|
a00b3d7fd1 | ||
|
|
c82c593a00 | ||
|
|
b7a39e92b3 | ||
|
|
8444d69eb9 | ||
|
|
295d7eaabb | ||
|
|
cf6a95c3b8 | ||
|
|
e315710292 | ||
|
|
2b35888297 | ||
|
|
0dfca163f0 | ||
|
|
30e5526fa7 | ||
|
|
196032c3fe | ||
|
|
a865fd1ad7 | ||
|
|
9c04238b16 | ||
|
|
25060e4397 | ||
|
|
a2979abd20 |
@@ -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}
|
||||
@@ -180,7 +181,7 @@
|
||||
clojure.core.async/take!
|
||||
clojure.core.async/to-chan!
|
||||
clojure.core.async/to-chan!!
|
||||
;; only writes .clj-kondo/ratchets.edn, from a :once fixture that finishes before the parallel tests
|
||||
;; writes the ratchet files from a :once fixture that finishes before the parallel tests
|
||||
dev.kondo-ratchet/fix!
|
||||
metabase.ai-tracing.core/record!
|
||||
metabase.api.macros.defendpoint.tools-manifest/assert-optional-fields-nullable!
|
||||
@@ -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
|
||||
@@ -803,7 +844,7 @@
|
||||
:name test-namespaces}
|
||||
{:pattern "^(?!.*-test(?:\\..*)?$)(?!.*test-util)(?!^metabase\\.test.*).*$"
|
||||
:name source-namespaces}
|
||||
{:pattern "(?:(?:^metabase\\.cmd.*)|(?:^build.*)|(?:^metabuild-common.*)|(?:^release.*)|(?:^i18n.*)|(?:^lint-migrations-file$))|(?:^mage.*)|(?:^load-namespaces.*)"
|
||||
{:pattern "(?:(?:^metabase\\.cmd.*)|(?:^build.*)|(?:^metabuild-common.*)|(?:^release.*)|(?:^i18n.*)|(?:^lint-migrations-file$))|(?:^mage.*)|(?:^load-namespaces.*)|(?:^dev\\..*)"
|
||||
:name printable-namespaces}
|
||||
{:pattern "^metabase\\.(?:(?:lib)|(?:legacy-mbql))\\..*"
|
||||
:name metabase-lib}
|
||||
@@ -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
|
||||
@@ -408,14 +416,18 @@
|
||||
auth-identity
|
||||
{:team "UX West"
|
||||
:api #{metabase.auth-identity.core
|
||||
metabase.auth-identity.init}
|
||||
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}}
|
||||
@@ -439,6 +451,7 @@
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
lib
|
||||
permissions
|
||||
queries
|
||||
util}
|
||||
@@ -465,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
|
||||
@@ -602,6 +613,7 @@
|
||||
audit-app
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -634,6 +646,7 @@
|
||||
documents
|
||||
eid-translation
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
notification
|
||||
@@ -666,6 +679,7 @@
|
||||
api
|
||||
channel
|
||||
events
|
||||
lib
|
||||
models
|
||||
request
|
||||
users
|
||||
@@ -688,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}
|
||||
@@ -810,6 +824,7 @@
|
||||
database-routing
|
||||
driver
|
||||
events
|
||||
lib
|
||||
request
|
||||
sync
|
||||
util
|
||||
@@ -838,6 +853,7 @@
|
||||
batch-processing
|
||||
collections
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
parameters
|
||||
@@ -962,6 +978,7 @@
|
||||
{:team "Embedding"
|
||||
:api #{metabase.embedding.init
|
||||
metabase.embedding.jwt
|
||||
metabase.embedding.schema
|
||||
metabase.embedding.settings
|
||||
metabase.embedding.util
|
||||
metabase.embedding.validation}
|
||||
@@ -1005,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
|
||||
@@ -1029,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
|
||||
@@ -1104,6 +1119,7 @@
|
||||
:api #{metabase.glossary.api}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/Glossary}
|
||||
@@ -1128,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
|
||||
@@ -1151,6 +1166,7 @@
|
||||
metabase.indexes.reconcile
|
||||
metabase.indexes.schema}
|
||||
:uses #{driver
|
||||
lib
|
||||
models
|
||||
util}
|
||||
:model-exports #{:model/TableIndex}}
|
||||
@@ -1160,6 +1176,7 @@
|
||||
:api #{metabase.indexes-rest.api}
|
||||
:uses #{api
|
||||
indexes
|
||||
lib
|
||||
transforms-base
|
||||
util}
|
||||
:model-imports #{:model/Database
|
||||
@@ -1179,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
|
||||
@@ -1317,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}
|
||||
@@ -1335,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}
|
||||
@@ -1343,6 +1359,7 @@
|
||||
api
|
||||
app-db
|
||||
config
|
||||
lib
|
||||
llm
|
||||
metabot
|
||||
oauth-server
|
||||
@@ -1356,7 +1373,7 @@
|
||||
|
||||
measures
|
||||
{:team "Graphy"
|
||||
:api #{metabase.measures.api}
|
||||
:api #{metabase.measures.api metabase.measures.schema}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
@@ -1377,11 +1394,11 @@
|
||||
metabase.metabot.core
|
||||
metabase.metabot.init
|
||||
metabase.metabot.metadata-perms
|
||||
metabase.metabot.schema
|
||||
metabase.metabot.self
|
||||
metabase.metabot.settings
|
||||
metabase.metabot.tools.entity-details}
|
||||
:friends #{agent-api
|
||||
enterprise/metabot
|
||||
enterprise/metabot-analytics
|
||||
slackbot}
|
||||
:uses #{activity-feed
|
||||
@@ -1429,6 +1446,7 @@
|
||||
transforms
|
||||
transforms-base
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses}
|
||||
:model-exports #{:model/AiUsageLog
|
||||
:model/Metabot
|
||||
@@ -1458,6 +1476,7 @@
|
||||
metabase.metrics.core}
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
@@ -1465,7 +1484,6 @@
|
||||
models
|
||||
parameters
|
||||
permissions
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
server
|
||||
@@ -1474,9 +1492,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
|
||||
@@ -1493,13 +1509,15 @@
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-exports #{:model/PersistedInfo}
|
||||
:model-imports #{:model/Card :model/Database}}
|
||||
|
||||
models
|
||||
{:team "DevEx"
|
||||
:api #{metabase.models.humanization
|
||||
:api #{metabase.models.db
|
||||
metabase.models.humanization
|
||||
metabase.models.init
|
||||
metabase.models.interface
|
||||
metabase.models.resolution
|
||||
@@ -1542,8 +1560,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 +1597,7 @@
|
||||
parameters
|
||||
permissions
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
session
|
||||
@@ -1612,6 +1630,7 @@
|
||||
api-scope
|
||||
appearance
|
||||
events
|
||||
lib
|
||||
mcp
|
||||
models
|
||||
request
|
||||
@@ -1682,6 +1701,7 @@
|
||||
remote-sync
|
||||
settings
|
||||
tracing
|
||||
users
|
||||
util
|
||||
enterprise/advanced-permissions}
|
||||
:model-exports #{:model/ApplicationPermissionsRevision
|
||||
@@ -1776,9 +1796,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 +1829,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 +1889,7 @@
|
||||
graph
|
||||
lib
|
||||
lib-be
|
||||
lib-metric
|
||||
metrics
|
||||
models
|
||||
parameters
|
||||
@@ -2040,8 +2057,10 @@
|
||||
metabase.remote-sync.init}
|
||||
:uses #{api
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features}
|
||||
premium-features
|
||||
util}
|
||||
:model-imports #{:model/Collection}}
|
||||
|
||||
request
|
||||
@@ -2055,7 +2074,8 @@
|
||||
api
|
||||
config
|
||||
embedding
|
||||
permissions ; FIXME
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
session
|
||||
settings
|
||||
@@ -2107,10 +2127,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 +2180,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 +2232,7 @@
|
||||
driver
|
||||
embedding
|
||||
initialization-status
|
||||
lib
|
||||
lib-be
|
||||
mcp
|
||||
oauth-server
|
||||
@@ -2228,6 +2262,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -2246,6 +2281,7 @@
|
||||
app-db
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
util
|
||||
@@ -2267,6 +2303,7 @@
|
||||
metabase.setup.init}
|
||||
:uses #{app-db
|
||||
config
|
||||
lib
|
||||
settings
|
||||
util}
|
||||
:model-imports #{:model/User}}
|
||||
@@ -2279,6 +2316,7 @@
|
||||
appearance
|
||||
auth-identity
|
||||
events
|
||||
lib
|
||||
request
|
||||
settings
|
||||
setup
|
||||
@@ -2349,10 +2387,10 @@
|
||||
metabase.sso.core
|
||||
metabase.sso.init
|
||||
metabase.sso.settings}
|
||||
:friends #{enterprise/sso}
|
||||
:uses #{api
|
||||
auth-identity
|
||||
config
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
@@ -2433,11 +2471,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 +2504,13 @@
|
||||
config
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
mcp
|
||||
metabot
|
||||
permissions
|
||||
premium-features
|
||||
search
|
||||
security-center
|
||||
session
|
||||
util}
|
||||
:model-imports #{:model/AiUsageLog
|
||||
@@ -2501,12 +2541,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}
|
||||
@@ -2555,8 +2595,7 @@
|
||||
tracing
|
||||
transforms-base
|
||||
util}
|
||||
:friends #{models
|
||||
enterprise/transforms-python}
|
||||
:friends #{enterprise/transforms-python}
|
||||
:model-exports #{:model/Transform
|
||||
:model/TransformDagRun
|
||||
:model/TransformJob
|
||||
@@ -2609,6 +2648,7 @@
|
||||
metabase.transforms-rest.api.transform-job
|
||||
metabase.transforms-rest.api.transform-tag}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
request
|
||||
transforms
|
||||
@@ -2670,6 +2710,7 @@
|
||||
driver
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
model-persistence
|
||||
models
|
||||
permissions
|
||||
@@ -2724,6 +2765,7 @@
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
notification
|
||||
permissions
|
||||
@@ -2750,6 +2792,7 @@
|
||||
collections
|
||||
config
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -2785,14 +2828,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 +2856,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 +2922,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 +2999,7 @@
|
||||
query-processor
|
||||
segments
|
||||
settings
|
||||
types
|
||||
util
|
||||
warehouse-schema}
|
||||
:model-imports #{:model/Card
|
||||
@@ -3007,7 +3053,8 @@
|
||||
setup
|
||||
sync
|
||||
users
|
||||
util}
|
||||
util
|
||||
warehouses}
|
||||
:model-imports #{:model/ApiKey :model/Database :model/User}}
|
||||
|
||||
enterprise/advanced-permissions
|
||||
@@ -3050,6 +3097,7 @@
|
||||
:api #{}
|
||||
:uses #{driver
|
||||
premium-features
|
||||
util
|
||||
enterprise/advanced-config
|
||||
enterprise/mfa
|
||||
enterprise/scim
|
||||
@@ -3084,6 +3132,7 @@
|
||||
permissions
|
||||
plugins
|
||||
premium-features
|
||||
queries
|
||||
query-permissions
|
||||
query-processor
|
||||
settings
|
||||
@@ -3092,6 +3141,8 @@
|
||||
sync
|
||||
users
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses
|
||||
enterprise/serialization}
|
||||
:model-imports #{:model/AuditLog
|
||||
:model/Card
|
||||
@@ -3124,6 +3175,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.billing.api.routes}
|
||||
:uses #{api
|
||||
lib
|
||||
premium-features
|
||||
store-api
|
||||
util}
|
||||
@@ -3133,6 +3185,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.cache.init}
|
||||
:uses #{cache
|
||||
lib
|
||||
premium-features
|
||||
query-processor
|
||||
task
|
||||
@@ -3185,6 +3238,7 @@
|
||||
:api #{metabase-enterprise.content-verification.api.routes}
|
||||
:uses #{api
|
||||
content-verification
|
||||
lib
|
||||
util
|
||||
enterprise/api}
|
||||
:model-imports #{:model/Card :model/Dashboard}}
|
||||
@@ -3216,15 +3270,28 @@
|
||||
:uses #{premium-features}}
|
||||
|
||||
enterprise/data-apps
|
||||
{:team "Gadget"
|
||||
{:team "Embedding"
|
||||
:api #{metabase-enterprise.data-apps.api
|
||||
metabase-enterprise.data-apps.init
|
||||
metabase-enterprise.data-apps.sync}
|
||||
:uses #{api
|
||||
collections
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
util}}
|
||||
util}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/DataPermissions
|
||||
:model/Database
|
||||
:model/Permissions
|
||||
:model/PermissionsGroup
|
||||
:model/Table
|
||||
:model/User}}
|
||||
|
||||
enterprise/data-complexity-score
|
||||
{:team "Metabot"
|
||||
@@ -3239,6 +3306,7 @@
|
||||
config
|
||||
driver
|
||||
embeddings
|
||||
lib
|
||||
metabot
|
||||
models
|
||||
premium-features
|
||||
@@ -3262,14 +3330,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 +3348,7 @@
|
||||
metabase-enterprise.database-replication.init}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
premium-features
|
||||
settings
|
||||
util
|
||||
@@ -3364,8 +3436,10 @@
|
||||
appearance
|
||||
audit-app
|
||||
embedding
|
||||
lib
|
||||
permissions
|
||||
premium-features
|
||||
util
|
||||
enterprise/sso}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -3393,6 +3467,7 @@
|
||||
collections
|
||||
entity-retrieval
|
||||
health-inspector
|
||||
lib
|
||||
premium-features
|
||||
search
|
||||
task
|
||||
@@ -3408,6 +3483,7 @@
|
||||
{:team "Gadget"
|
||||
:api #{metabase-enterprise.erd.api}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
util
|
||||
@@ -3423,9 +3499,11 @@
|
||||
:uses #{analytics
|
||||
analytics-interface
|
||||
api
|
||||
lib
|
||||
premium-features
|
||||
settings
|
||||
util
|
||||
warehouses
|
||||
enterprise/harbormaster}
|
||||
:model-imports #{:model/Database :model/Setting}}
|
||||
|
||||
@@ -3463,13 +3541,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
|
||||
@@ -3497,10 +3578,8 @@
|
||||
:api #{metabase-enterprise.metabot.api
|
||||
metabase-enterprise.metabot.api.routes
|
||||
metabase-enterprise.metabot.init}
|
||||
:friends #{slackbot
|
||||
mcp
|
||||
metabot}
|
||||
:uses #{api
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
@@ -3524,6 +3603,7 @@
|
||||
:api #{metabase-enterprise.metabot-analytics.api}
|
||||
:uses #{api
|
||||
driver
|
||||
lib
|
||||
metabot
|
||||
permissions
|
||||
query-processor
|
||||
@@ -3540,9 +3620,11 @@
|
||||
metabase-enterprise.mfa.routes}
|
||||
:uses #{api
|
||||
appearance
|
||||
auth-identity
|
||||
channel
|
||||
config
|
||||
events
|
||||
lib
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -3577,6 +3659,7 @@
|
||||
app-db
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
premium-features
|
||||
search
|
||||
@@ -3611,12 +3694,15 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
app-db
|
||||
dashboards
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
measures
|
||||
model-persistence
|
||||
models
|
||||
queries
|
||||
segments
|
||||
source-swap
|
||||
task
|
||||
transforms
|
||||
@@ -3636,8 +3722,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 +3734,7 @@
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
request
|
||||
tenants
|
||||
@@ -3674,6 +3760,7 @@
|
||||
:uses #{analytics-interface
|
||||
api
|
||||
api-keys
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
server
|
||||
@@ -3694,8 +3781,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 +3790,12 @@
|
||||
config
|
||||
driver
|
||||
events
|
||||
lib
|
||||
models
|
||||
notification
|
||||
permissions
|
||||
premium-features
|
||||
security-center
|
||||
settings
|
||||
system
|
||||
task
|
||||
@@ -3735,12 +3823,14 @@
|
||||
embeddings
|
||||
events
|
||||
health-inspector
|
||||
lib
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
search
|
||||
settings
|
||||
task
|
||||
@@ -3778,7 +3868,8 @@
|
||||
enterprise/snippet-collections
|
||||
{:team "Graphy"
|
||||
:api #{}
|
||||
:uses #{models
|
||||
:uses #{lib
|
||||
models
|
||||
native-query-snippets
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3814,6 +3905,7 @@
|
||||
:uses #{analytics
|
||||
api
|
||||
collections
|
||||
lib
|
||||
premium-features
|
||||
queries
|
||||
request
|
||||
@@ -3830,11 +3922,13 @@
|
||||
app-db
|
||||
auth-identity
|
||||
events
|
||||
lib
|
||||
models
|
||||
request
|
||||
settings
|
||||
system
|
||||
task
|
||||
users
|
||||
util}
|
||||
:model-imports #{:model/AuthIdentity :model/Session :model/User}}
|
||||
|
||||
@@ -3846,6 +3940,7 @@
|
||||
auth-identity
|
||||
collections
|
||||
events
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
@@ -3858,7 +3953,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 +4011,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.upload-management.api}
|
||||
:uses #{api
|
||||
lib
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
|
||||
6
.clj-kondo/config/modules/ratchets.edn
Normal file
@@ -0,0 +1,6 @@
|
||||
;; Budgets for escape hatches in config.edn. Counts may fall but not exceed these values.
|
||||
;; Leave reductions to the post-merge shrink workflow; explain increases in the PR.
|
||||
{:api-any 1
|
||||
:friend-edges 22
|
||||
:model-imports-bypass 2
|
||||
:uses-any 4}
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Any ignore outside :comment-exempt needs an explanatory comment directly above or trailing on its line.
|
||||
;; `./bin/mage kondo-ratchets-shrink` lowers budgets unless `--seed` explicitly adds or raises one.
|
||||
;; The workflow runs it on master, so feature branches do not need to record reductions.
|
||||
;; Raising or adding a budget (`--seed` for inline ignores, a manual edit for config) or widening the
|
||||
;; Raising or adding a budget (`--seed` for inline ignores, a manual edit otherwise) or widening the
|
||||
;; exemptions must be explained in the PR.
|
||||
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
|
||||
{:ignore-counts {:aliased-namespace-symbol 3
|
||||
@@ -21,7 +21,7 @@
|
||||
:duplicate-require 1
|
||||
:equals-true 1
|
||||
:main-without-gen-class 1
|
||||
:metabase/defsetting-namespace 2
|
||||
:metabase/defsetting-namespace 1
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests 34
|
||||
:metabase/discourage-millis-duration 1
|
||||
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests 13
|
||||
@@ -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
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
(defn- lint-modules [ns-form-node config]
|
||||
(let [ns-symb (ns-form-node->ns-symb ns-form-node)]
|
||||
(when-not (modules/ignored-namespace? config ns-symb)
|
||||
(when-let [current-module (modules/module ns-symb)]
|
||||
(when-let [current-module (modules/module config ns-symb)]
|
||||
(let [required-namespace-symb-nodes (-> ns-form-node
|
||||
ns-form-node->require-node
|
||||
require-node->namespace-symb-nodes)]
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
(every? simple-symbol? required-namespaces)]}
|
||||
(when-not (modules/ignored-namespace? config current-ns)
|
||||
;; ignore namespaces outside of the module system
|
||||
(when-let [current-module (modules/module current-ns)]
|
||||
(when-let [current-module (modules/module config current-ns)]
|
||||
(doseq [required-namespace required-namespaces]
|
||||
;; ignore namespaces outside of the module system.
|
||||
(when (modules/module required-namespace)
|
||||
(when (modules/module config required-namespace)
|
||||
(when-let [error (modules/usage-error config current-module required-namespace)]
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message error
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
(defn- lint-dynamic-require* [node current-ns config]
|
||||
(when (and (not (modules/ignored-namespace? config current-ns))
|
||||
(modules/module current-ns))
|
||||
(modules/module config current-ns))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "Module dependency cannot be statically determined from a dynamic require"
|
||||
:type :metabase/modules))))
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
(not (contains? @warned-namespaces ns-symb)))
|
||||
(swap! warned-namespaces conj ns-symb)
|
||||
(let [known-modules (set (keys (:metabase/modules (modules/config input))))
|
||||
current-module (modules/module ns-symb)]
|
||||
current-module (modules/module (modules/config input) ns-symb)]
|
||||
(when (or (not current-module)
|
||||
(not (contains? known-modules current-module)))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
(ns hooks.common.modules
|
||||
"Resolves namespaces to modules and enforces module boundaries.
|
||||
|
||||
The Kondo linter, config generator, and CI tooling share this namespace so
|
||||
they apply the same rules."
|
||||
(:require
|
||||
[clojure.string :as str]))
|
||||
|
||||
@@ -12,25 +16,130 @@
|
||||
(merge (get-in config [:linters :metabase/modules])
|
||||
(select-keys config [:metabase/modules])))
|
||||
|
||||
(defn module
|
||||
;;;; Module tree. These functions take the `:metabase/modules` map, or the prefix map built from it.
|
||||
;;;;
|
||||
;;;; Dots express nesting: `lib.schema` is a child of `lib`. A declared
|
||||
;;;; `enterprise/X` module is also a child of OSS module `X`. A namespace
|
||||
;;;; belongs to the module with the most specific matching `:ns-prefix`.
|
||||
|
||||
(defn default-ns-prefix
|
||||
"Namespace prefix a module owns unless it sets `:ns-prefix`: `metabase.lib.schema` for `lib.schema`,
|
||||
`metabase-enterprise.foo` for `enterprise/foo`."
|
||||
[module]
|
||||
(str (if (= "enterprise" (namespace module)) "metabase-enterprise." "metabase.") (name module)))
|
||||
|
||||
(defn module-ns-prefix
|
||||
"Namespace prefix `module` owns: its `:ns-prefix`, else [[default-ns-prefix]]."
|
||||
[modules module]
|
||||
(or (get-in modules [module :ns-prefix])
|
||||
(default-ns-prefix module)))
|
||||
|
||||
(defn build-prefix->module
|
||||
"Map each declared namespace prefix to its module, for [[declared-module]]."
|
||||
[modules]
|
||||
(into {}
|
||||
(map (fn [module] [(module-ns-prefix modules module) module]))
|
||||
(keys modules)))
|
||||
|
||||
(defn declared-module
|
||||
"Resolve `ns-symb` to its declared module, or `nil`.
|
||||
|
||||
Chooses the most specific dotted prefix and ignores a trailing `-test`."
|
||||
[prefix->module ns-symb]
|
||||
(loop [candidate (str/replace (str ns-symb) #"-test$" "")]
|
||||
(or (get prefix->module candidate)
|
||||
(when-let [dot (str/last-index-of candidate ".")]
|
||||
(recur (subs candidate 0 dot))))))
|
||||
|
||||
(defn resolve-module
|
||||
"E.g.
|
||||
|
||||
(module 'metabase.qp.middleware.wow) => 'qp
|
||||
(module 'metabase-enterprise.whatever.core) => enterprise/whatever"
|
||||
[ns-symb]
|
||||
(resolve-module prefix->module 'metabase.qp.middleware.wow) => 'qp
|
||||
(resolve-module prefix->module 'metabase-enterprise.whatever.core) => enterprise/whatever
|
||||
|
||||
An unmatched Metabase namespace resolves to its first segment so the linter can report an undeclared module."
|
||||
[prefix->module ns-symb]
|
||||
{:pre [(simple-symbol? ns-symb)]}
|
||||
;; treat something like `metabase.driver-test` (for a module that hasn't fully been updated to use `.core`
|
||||
;; namespaces) as being in the `driver` module
|
||||
(let [ns-symb (if (str/ends-with? (name ns-symb) "-test")
|
||||
(symbol (str/replace (name ns-symb) #"-test$" ""))
|
||||
ns-symb)]
|
||||
(or (some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
|
||||
(or (declared-module prefix->module ns-symb)
|
||||
(some->> (re-find #"^metabase-enterprise\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
(symbol "enterprise"))
|
||||
(some-> (re-find #"^metabase\.([^.]+)" (str ns-symb))
|
||||
second
|
||||
symbol))))
|
||||
|
||||
(defn parent-module
|
||||
"The direct parent of `module`, or `nil` when it is top-level."
|
||||
[modules module]
|
||||
(let [module-name (name module)]
|
||||
(if-let [dot (str/last-index-of module-name ".")]
|
||||
(symbol (namespace module) (subs module-name 0 dot))
|
||||
(when (= "enterprise" (namespace module))
|
||||
(let [oss (symbol module-name)]
|
||||
(when (contains? modules oss)
|
||||
oss))))))
|
||||
|
||||
(defn descendant-of?
|
||||
"Whether `module` is `ancestor` or one of its descendants."
|
||||
[modules module ancestor]
|
||||
(boolean (some #{ancestor} (take-while some? (iterate #(parent-module modules %) module)))))
|
||||
|
||||
(defn- exports-child?
|
||||
[modules parent child]
|
||||
(or (contains? (set (get-in modules [parent :module-exports])) child)
|
||||
;; OSS module `X` implicitly exports its `enterprise/X` companion.
|
||||
(and (not= (namespace parent) (namespace child))
|
||||
(contains? modules child))))
|
||||
|
||||
(defn- blocking-export
|
||||
"The first missing export from `module` toward the root.
|
||||
|
||||
Returns `{:ancestor parent, :child child}`, or `nil` when every link is
|
||||
exported."
|
||||
[modules module]
|
||||
(loop [child module]
|
||||
(when-let [ancestor (parent-module modules child)]
|
||||
(if (exports-child? modules ancestor child)
|
||||
(recur ancestor)
|
||||
{:ancestor ancestor, :child child}))))
|
||||
|
||||
(defn namability-error
|
||||
"Explain why `caller` may not refer to `target` in `:uses`, or return `nil` if it may.
|
||||
|
||||
A nested module is private to the nearest ancestor with a missing export.
|
||||
Exporting every link makes it public."
|
||||
[modules caller target]
|
||||
(when-let [{:keys [ancestor child]} (blocking-export modules target)]
|
||||
(when-not (descendant-of? modules caller ancestor)
|
||||
(format (str "Module %s is nested and not exported by its ancestors; %s may not use it. Add %s to %s's "
|
||||
":module-exports, or move the caller into the %s subtree. [:metabase/modules %s :module-exports]")
|
||||
target caller child ancestor ancestor ancestor))))
|
||||
|
||||
;;;; Lint rules. These functions take the full linter config.
|
||||
|
||||
(defonce ^:private prefix->module-cache
|
||||
(atom nil))
|
||||
|
||||
(defn- prefix->module
|
||||
"[[build-prefix->module]] for `config`, cached because Kondo hands every hook call the same `:metabase/modules` map."
|
||||
[config]
|
||||
(let [modules (:metabase/modules config)
|
||||
cached @prefix->module-cache]
|
||||
(if (identical? modules (:modules cached))
|
||||
(:prefix->module cached)
|
||||
(:prefix->module (reset! prefix->module-cache {:modules modules
|
||||
:prefix->module (build-prefix->module modules)})))))
|
||||
|
||||
(defn module
|
||||
"Resolve `ns-symb` to its module under `config`; see [[resolve-module]]."
|
||||
[config ns-symb]
|
||||
(resolve-module (prefix->module config) ns-symb))
|
||||
|
||||
(defn- module-api-namespaces
|
||||
"Set of API namespace symbols for a given module. `:any` means you can use anything, there are no API namespaces for
|
||||
this module (yet). If unspecified, the default is just the `<module>.core` namespace."
|
||||
@@ -44,9 +153,7 @@
|
||||
module-config
|
||||
|
||||
:else
|
||||
(let [ns-prefix (if (= (namespace module) "enterprise")
|
||||
(str "metabase-enterprise." (name module))
|
||||
(name module))]
|
||||
(let [ns-prefix (module-ns-prefix (:metabase/modules config) module)]
|
||||
#{(symbol (str ns-prefix ".api"))
|
||||
(symbol (str ns-prefix ".core"))
|
||||
(symbol (str ns-prefix ".init"))}))))
|
||||
@@ -68,15 +175,17 @@
|
||||
(contains? (set allowed-modules) required-module))))
|
||||
|
||||
(defn- allowed-module-namespace? [config current-module ns-symb]
|
||||
(let [module (module ns-symb)
|
||||
(let [module (module config ns-symb)
|
||||
module-api-namespaces (module-api-namespaces config module)
|
||||
module-friends (module-friends config module)]
|
||||
(or (nil? module-api-namespaces)
|
||||
(contains? module-api-namespaces ns-symb)
|
||||
(contains? module-friends current-module))))
|
||||
(contains? module-friends current-module)
|
||||
;; a child may use its ancestors' internals; a parent still goes through its child's `:api`
|
||||
(descendant-of? (:metabase/modules config) current-module module))))
|
||||
|
||||
(defn- rest-module? [module]
|
||||
(str/ends-with? module "-rest"))
|
||||
(re-find #"[.-]rest$" (str module)))
|
||||
|
||||
(defn- routes-module? [module]
|
||||
(str/ends-with? module "-routes"))
|
||||
@@ -89,7 +198,7 @@
|
||||
error type if there is one, otherwise `nil` if there are no errors."
|
||||
[config current-module required-namespace]
|
||||
;; ignore stuff not in a module i.e. non-Metabase stuff.
|
||||
(when-let [required-module (module required-namespace)]
|
||||
(when-let [required-module (module config required-namespace)]
|
||||
(when-not (= current-module required-module)
|
||||
(cond
|
||||
(not (allowed-module? config current-module required-module))
|
||||
@@ -113,4 +222,8 @@
|
||||
required-module
|
||||
current-module
|
||||
required-module
|
||||
(symbol (str/replace required-module #"-rest$" "")))))))
|
||||
(symbol (str/replace required-module #"[.-]rest$" "")))
|
||||
|
||||
;; Config tests check explicit `:uses`; wildcard callers are checked here.
|
||||
(= :any (allowed-modules config current-module))
|
||||
(namability-error (:metabase/modules config) current-module required-module)))))
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
[hooks.common :as common]))
|
||||
|
||||
(defn lint-defsetting-namespace [node context]
|
||||
(when-not (re-matches #"^metabase(?:-enterprise)?\.[^\.]+\.settings$" (name (:ns context)))
|
||||
(when-not (re-matches #"^metabase(?:-enterprise)?\.([^\.]+\.)+settings$" (name (:ns context)))
|
||||
(hooks/reg-finding! (assoc (meta node)
|
||||
:message "All defsettings should live in metabase[-enterprise].<module>.settings namespaces"
|
||||
:message "All defsettings should live in a namespace ending in .settings under metabase[-enterprise]"
|
||||
:type :metabase/defsetting-namespace))))
|
||||
|
||||
;;; TODO -- move this into a Kondo config file in `.clj-kondo/config/`
|
||||
|
||||
45
.clj-kondo/src/hooks/metabase/toucan/db_ns.clj
Normal file
@@ -0,0 +1,45 @@
|
||||
(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]
|
||||
[hooks.common.modules :as modules]))
|
||||
|
||||
(def ^:private driver-db-namespace
|
||||
"Matches `metabase.driver.<driver>.db`; individual drivers are not modules."
|
||||
#"^metabase\.driver\.[^.]+\.db$")
|
||||
|
||||
(defn- db-namespace? [config ns-sym]
|
||||
(let [ns-str (name ns-sym)]
|
||||
(boolean
|
||||
;; Resolve ownership first: a nested `.db` name alone does not make a module.
|
||||
(or (when-let [module (modules/module config ns-sym)]
|
||||
(= ns-str (str (modules/module-ns-prefix (:metabase/modules config) module) ".db")))
|
||||
(re-matches driver-db-namespace ns-str)))))
|
||||
|
||||
(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? (modules/config input) 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)
|
||||
@@ -5,6 +5,6 @@
|
||||
|
||||
(deftest ^:parallel module-test
|
||||
(are [symb expected] (= expected
|
||||
(hooks.common.modules/module symb))
|
||||
(hooks.common.modules/module {} symb))
|
||||
'metabase.qp.middleware.wow 'qp
|
||||
'metabase-enterprise.whatever.core 'enterprise/whatever))
|
||||
|
||||
73
.clj-kondo/test/hooks/metabase/toucan/db_ns_test.clj
Normal file
@@ -0,0 +1,73 @@
|
||||
(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 modules]]
|
||||
(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")
|
||||
:config {:metabase/modules modules}}
|
||||
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)))))
|
||||
|
||||
(deftest ^:parallel t2-query-namespace-follows-the-module-tree-test
|
||||
(let [modules '{queries {}
|
||||
metabot {}
|
||||
metabot.llm {}
|
||||
lib.be {:ns-prefix "metabase.lib-be"}
|
||||
enterprise/sandbox {}}]
|
||||
(testing "a nested module owns its own db namespace"
|
||||
(is (empty? (lint-query-call '(t2/select :model/Card) 'metabase.metabot.llm.db nil modules))))
|
||||
(testing "the parent's db namespace stays its own"
|
||||
(is (empty? (lint-query-call '(t2/select :model/Card) 'metabase.metabot.db nil modules))))
|
||||
(testing "a module with an explicit :ns-prefix is resolved through it, not its dotted name"
|
||||
(is (empty? (lint-query-call '(t2/select :model/Card) 'metabase.lib-be.db nil modules)))
|
||||
(is (=? [{:type :metabase/t2-query-namespace}]
|
||||
(lint-query-call '(t2/select :model/Card) 'metabase.lib.be.db nil modules))))
|
||||
(testing "a db namespace under a directory naming no module is still a finding"
|
||||
(is (=? [{:type :metabase/t2-query-namespace}]
|
||||
(lint-query-call '(t2/query {:select [:*]}) 'metabase.queries.models.db nil modules)))
|
||||
(is (=? [{:type :metabase/t2-query-namespace}]
|
||||
(lint-query-call '(t2/query {:select [:*]}) 'metabase.metabot.llm.models.db nil modules))))
|
||||
(testing "enterprise modules resolve through the metabase-enterprise root"
|
||||
(is (empty? (lint-query-call '(t2/select :model/Card) 'metabase-enterprise.sandbox.db nil modules))))))
|
||||
3
.github/actionlint.yaml
vendored
@@ -14,3 +14,6 @@ paths:
|
||||
.github/workflows/lint.yml:
|
||||
ignore:
|
||||
- 'step must run script with "run" section or run action with "uses" section'
|
||||
.github/workflows/pr.checks.yml:
|
||||
ignore:
|
||||
- 'step must run script with "run" section or run action with "uses" section'
|
||||
|
||||
29
.github/actions/fetch-artifact/action.yml
vendored
@@ -20,6 +20,15 @@ inputs:
|
||||
description: Directory path where the jar should be extracted (defaults to current directory)
|
||||
required: false
|
||||
default: "."
|
||||
download:
|
||||
description: Default is true. Set to false to only look the artifact up and report `fetch-hit`.
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
outputs:
|
||||
fetch-hit:
|
||||
description: Boolean indicating if an artifact was successfully found and downloaded.
|
||||
value: ${{ steps.find_release_artifact.outputs.fetch-hit }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -28,11 +37,12 @@ runs:
|
||||
id: find_release_artifact
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
result-encoding: string
|
||||
script: | # js
|
||||
const fs = require('fs');
|
||||
|
||||
const isRelease = "${{ inputs.release }}" === "true";
|
||||
const failOnMiss = "${{ inputs.fail-on-fetch-miss }}" !== "false";
|
||||
const shouldDownload = "${{ inputs.download }}" !== "false";
|
||||
const prefix = isRelease ? "metabase-release" : "metabase";
|
||||
const artifactName = `${prefix}-${{ inputs.edition }}-${{ inputs.commit }}-uberjar`;
|
||||
console.log(`Looking for artifact: ${artifactName}`);
|
||||
@@ -44,12 +54,18 @@ runs:
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (!artifacts.data?.artifacts?.[0]?.id) {
|
||||
core.setFailed(`No artifacts found for ${artifactName}`);
|
||||
return;
|
||||
const artifact_id = artifacts.data?.artifacts?.[0]?.id;
|
||||
|
||||
if (!artifact_id) {
|
||||
core.setOutput('fetch-hit', 'false');
|
||||
} else {
|
||||
core.setOutput('fetch-hit', 'true');
|
||||
}
|
||||
|
||||
const artifact_id = artifacts.data.artifacts[0].id;
|
||||
if (!shouldDownload) {
|
||||
console.log(`skipping download (download: false)`);
|
||||
return;
|
||||
}
|
||||
|
||||
let download;
|
||||
const maxAttempts = 3;
|
||||
@@ -74,6 +90,7 @@ runs:
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/mb.zip`, Buffer.from(download.data));
|
||||
|
||||
- name: Extract and rename JAR
|
||||
if: ${{ steps.find_release_artifact.outputs.fetch-hit == 'true' && inputs.download != 'false' }}
|
||||
shell: bash
|
||||
run: | # bash
|
||||
OUTPUT_NAME="${{ inputs.output-name }}"
|
||||
@@ -119,12 +136,14 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Verify that this is a valid JAR file
|
||||
if: ${{ steps.find_release_artifact.outputs.fetch-hit == 'true' && inputs.download != 'false' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Verifying JAR at: $JAR_PATH"
|
||||
file --mime-type "$JAR_PATH"
|
||||
|
||||
- name: Reveal its version.properties
|
||||
if: ${{ steps.find_release_artifact.outputs.fetch-hit == 'true' && inputs.download != 'false' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Extracting version.properties from: $JAR_PATH"
|
||||
|
||||
51
.github/actions/get-docker-info/action.yml
vendored
@@ -1,7 +1,15 @@
|
||||
name: Get Docker Info
|
||||
description: Determines Docker repository and tag based on branch and edition
|
||||
description: Determines the registry, repository, and tag a container image is pushed to.
|
||||
|
||||
inputs:
|
||||
repo:
|
||||
description: Registry and repo, e.g. "metabase/metabase". Overrides branch derived repo.
|
||||
required: false
|
||||
default: ''
|
||||
tag:
|
||||
description: Docker tag, e.g. v1.87.2. Overrides a branch derived tag.
|
||||
required: false
|
||||
default: ''
|
||||
branch:
|
||||
description: Branch name (defaults to current branch)
|
||||
required: false
|
||||
@@ -25,9 +33,6 @@ outputs:
|
||||
full_image:
|
||||
description: Full Docker image name (repo:tag)
|
||||
value: ${{ steps.docker-info.outputs.full_image }}
|
||||
editions:
|
||||
description: JSON array of editions to build
|
||||
value: ${{ steps.docker-info.outputs.editions }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -41,34 +46,22 @@ runs:
|
||||
const edition = '${{ inputs.edition }}';
|
||||
const owner = '${{ inputs.owner }}';
|
||||
|
||||
console.log(`Determining Docker info for branch: ${branch}, edition: ${edition}`);
|
||||
|
||||
let repo, tag, editions;
|
||||
|
||||
if (branch === 'master') {
|
||||
// Master builds both editions with specific repos
|
||||
editions = ['ee', 'oss'];
|
||||
tag = 'latest';
|
||||
|
||||
if (edition === 'ee') {
|
||||
repo = `${owner}/metabase-enterprise-head`;
|
||||
} else {
|
||||
repo = `${owner}/metabase-head`;
|
||||
}
|
||||
} else {
|
||||
// All other branches only build EE to metabase-dev
|
||||
editions = ['ee'];
|
||||
tag = branch.replaceAll('/', '-'); // slashes make docker sad
|
||||
repo = `${owner}/metabase-dev`;
|
||||
let tag = '${{ inputs.tag}}';
|
||||
if (!tag) {
|
||||
tag = branch === 'master' ? 'latest' : `${branch.replaceAll('/', '-')}-${edition}`;
|
||||
}
|
||||
|
||||
const fullImage = `${repo}:${tag}`;
|
||||
let repo = '${{ inputs.repo }}';
|
||||
if (!repo) {
|
||||
if (branch === 'master') {
|
||||
repo = edition === 'ee' ? `${owner}/metabase-enterprise-head` : `${owner}/metabase-head`;
|
||||
} else {
|
||||
repo = `${owner}/metabase-dev`;
|
||||
}
|
||||
}
|
||||
|
||||
// Set outputs
|
||||
core.setOutput('repo', repo);
|
||||
core.setOutput('tag', tag);
|
||||
core.setOutput('full_image', fullImage);
|
||||
core.setOutput('editions', JSON.stringify(editions));
|
||||
core.setOutput('full_image', `${repo}:${tag}`);
|
||||
|
||||
console.log(`Docker image: ${fullImage}`);
|
||||
console.log(`Editions: ${JSON.stringify(editions)}`);
|
||||
console.log(`Resolved: repo = ${repo}, tag = ${tag}`);
|
||||
|
||||
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"
|
||||
|
||||
42
.github/actions/test-driver/action.yml
vendored
@@ -20,8 +20,30 @@ inputs:
|
||||
required: false
|
||||
default: "ee"
|
||||
description: Edition to test (oss or ee)
|
||||
quarantine-engine:
|
||||
runner-index:
|
||||
required: false
|
||||
description: >-
|
||||
The index of this runner among those sharing a partitioned suite. See
|
||||
`be-tests-bigquery-cloud-sdk-ee` in `.github/workflows/drivers.yml` for an example.
|
||||
runner-count:
|
||||
required: false
|
||||
description: >-
|
||||
How many runners share the suite. Recommended to set to `strategy.job-total` rather
|
||||
than a literal so that it doesn't need updating if the `runner-index` matrix changes.
|
||||
jvms-per-runner:
|
||||
required: false
|
||||
default: "1"
|
||||
description: >-
|
||||
Concurrent JVMs per runner. Recommended to raise above 1 for drivers whose tests
|
||||
spend a lot of time waiting on a remote warehouse.
|
||||
heap-per-jvm:
|
||||
required: false
|
||||
default: "4g"
|
||||
description: >-
|
||||
`-Xmx` per JVM, applied only when `jvms-per-runner` is more than 1 (otherwise the
|
||||
the `:ci` alias's `-Xmx12g` is used). To size it: reserve ~1G for the OS and ~2G
|
||||
of non-heap per JVM, then divide what's left. On a 16G runner that's ~4G each for
|
||||
two JVMs.
|
||||
granular-rerun:
|
||||
required: false
|
||||
default: "true"
|
||||
@@ -100,6 +122,7 @@ runs:
|
||||
# Otherwise run exactly as before. Quote the file so the whole `[ns/var ...]`
|
||||
# vector reaches clojure -X as a single argument.
|
||||
run: |
|
||||
ALIASES="dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test"
|
||||
BASE="clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test"
|
||||
# `:profile-tests` overrides `:test`'s exec-fn, so it has to come last. See dev.profile-tests.
|
||||
# The `profiled` output is what the collection steps below key off, so that the decision is
|
||||
@@ -117,12 +140,19 @@ runs:
|
||||
$BASE :only "$ONLY"
|
||||
elif [ "${{ inputs.granular-rerun }}" = "true" ] && [ "${{ github.run_attempt }}" != "1" ] && [ -s target/failed-tests ]; then
|
||||
echo "Granular rerun of previously-failed tests: $(cat target/failed-tests)"
|
||||
$BASE :only "$(cat target/failed-tests)"
|
||||
clojure -X:"$ALIASES" :only "$(cat target/failed-tests)"
|
||||
elif [ -n "$TEST_RUNNER_INDEX" ]; then
|
||||
CLOJURE_ALIASES="$ALIASES" ./.github/scripts/run-backend-test-jvms.sh ${{ inputs.test-args }}
|
||||
else
|
||||
$BASE ${{ inputs.test-args }}
|
||||
clojure -X:"$ALIASES" ${{ inputs.test-args }}
|
||||
fi
|
||||
shell: bash
|
||||
id: run-test
|
||||
env:
|
||||
TEST_RUNNER_INDEX: ${{ inputs.runner-index }}
|
||||
TEST_RUNNER_COUNT: ${{ inputs.runner-count }}
|
||||
TEST_JVMS_PER_RUNNER: ${{ inputs.jvms-per-runner }}
|
||||
TEST_HEAP_PER_JVM: ${{ inputs.heap-per-jvm }}
|
||||
# Keep this step continue-on-error because the quarantine gate below controls whether it
|
||||
# ultimately fails.
|
||||
continue-on-error: true
|
||||
@@ -186,7 +216,9 @@ runs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ format('test-logs-{0}{1}', github.job, inputs.artifact-suffix && format('-{0}', inputs.artifact-suffix) || '') }}
|
||||
path: logs/test-log.json
|
||||
# Glob: a single-JVM run writes `logs/test-log.json`; a multi-JVM run merges one
|
||||
# `logs/test-log-p<index>.json` per JVM.
|
||||
path: logs/test-log*.json
|
||||
retention-days: 1
|
||||
# Failing to upload the artifact should never affect the outcome of the whole test run.
|
||||
continue-on-error: true
|
||||
@@ -199,4 +231,4 @@ runs:
|
||||
with:
|
||||
adapter: backend
|
||||
test-suite: ${{ inputs.junit-name }}
|
||||
enforce-quarantine: ${{ inputs.quarantine-engine == 'ci-conductor' }}
|
||||
enforce-quarantine: "true"
|
||||
|
||||
9
.github/file-paths.yaml
vendored
@@ -115,10 +115,10 @@ backend_sources: &backend_sources
|
||||
- "src/**"
|
||||
- "modules/drivers/{*,*/{*,!(test)/**}}"
|
||||
- "resources/**"
|
||||
# The ratchets file drives project_ratchet_checks alone, so the automation PR that lowers the budgets
|
||||
# runs the Project tests job by itself. Patterns are OR-ed, so a separate "!..." entry would match
|
||||
# Ratchet files drive project_ratchet_checks alone, so automation PRs that lower budgets
|
||||
# run the Project tests job by themselves. Patterns are OR-ed, so a separate "!..." entry would match
|
||||
# every other path instead of excluding this one.
|
||||
- ".clj-kondo/{!(ratchets.edn),*/**}"
|
||||
- ".clj-kondo/**/!(ratchets.edn)"
|
||||
|
||||
backend_specs: &backend_specs
|
||||
- *shared_specs
|
||||
@@ -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"
|
||||
@@ -167,6 +169,7 @@ project_backend_checks:
|
||||
# Only the ratchet check reads the budgets, so a diff that touches nothing else runs that suite alone.
|
||||
project_ratchet_checks:
|
||||
- ".clj-kondo/ratchets.edn"
|
||||
- ".clj-kondo/config/modules/ratchets.edn"
|
||||
|
||||
project_migration_checks:
|
||||
- *project_tests_infra
|
||||
|
||||
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())
|
||||
|
||||
11
.github/scripts/file-paths.unit.spec.ts
vendored
@@ -29,7 +29,7 @@ describe("file-paths.yaml", () => {
|
||||
// dorny/paths-filter ORs the patterns in a filter, so a "!..." entry does not subtract from the
|
||||
// other entries - it matches every path they don't, turning the filter permanently on. Excluding
|
||||
// a path means writing the exclusion inside one pattern, the way backend_sources keeps
|
||||
// ratchets.edn out with ".clj-kondo/{!(ratchets.edn),*/**}".
|
||||
// ratchet files out with ".clj-kondo/**/!(ratchets.edn)".
|
||||
it("never excludes a path with a standalone negation", () => {
|
||||
const negated = Object.entries(filters).flatMap(([name, filter]) =>
|
||||
patterns(filter)
|
||||
@@ -44,6 +44,9 @@ describe("file-paths.yaml", () => {
|
||||
"%s skips a ratchets-only change and still follows the rest of .clj-kondo",
|
||||
(name) => {
|
||||
expect(matches(name, ".clj-kondo/ratchets.edn")).toBe(false);
|
||||
expect(
|
||||
matches(name, ".clj-kondo/config/modules/ratchets.edn"),
|
||||
).toBe(false);
|
||||
expect(matches(name, ".clj-kondo/config.edn")).toBe(true);
|
||||
expect(matches(name, ".clj-kondo/config/modules/config.edn")).toBe(true);
|
||||
},
|
||||
@@ -53,5 +56,11 @@ describe("file-paths.yaml", () => {
|
||||
expect(matches("project_ratchet_checks", ".clj-kondo/ratchets.edn")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
matches(
|
||||
"project_ratchet_checks",
|
||||
".clj-kondo/config/modules/ratchets.edn",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
160
.github/scripts/run-backend-test-jvms.sh
vendored
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run this runner's share of a partitioned backend test suite as one or more concurrent
|
||||
# JVMs. The caller says how many runners share the suite and how many JVMs each gets.
|
||||
#
|
||||
# WHY MULTIPLE JVMS PER RUNNER
|
||||
# ----------------------------
|
||||
# Driver tests spend most of their wall time waiting on the remote warehouse, and they
|
||||
# can't use the idle CPU by using more threads because some tests mutate global state.
|
||||
# A second JVM gets that isolation for free.
|
||||
#
|
||||
# Two paths are NOT safe to share:
|
||||
# * `target/junit` — hawk deletes the whole directory at `:begin-test-run`, so a JVM
|
||||
# starting late would wipe an earlier one's results.
|
||||
# * `logs/test-log.json` — the log4j2 File appender truncates it on open.
|
||||
# So each JVM runs in its own hardlinked copy of the checkout (inodes, not disk) and
|
||||
# outputs are merged back into the workspace afterwards.
|
||||
#
|
||||
# INPUTS (environment)
|
||||
# --------------------
|
||||
# CLOJURE_ALIASES alias string for `clojure -X`, e.g. dev:ci:ee:ee-dev:drivers:drivers-dev:test
|
||||
# TEST_RUNNER_INDEX index of this runner among those sharing the suite
|
||||
# TEST_RUNNER_COUNT how many runners share the suite
|
||||
# TEST_JVMS_PER_RUNNER concurrent JVMs per runner (default 1)
|
||||
# TEST_HEAP_PER_JVM -Xmx per JVM, applied only when running more than one
|
||||
#
|
||||
# Exits non-zero if any JVM fails.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
: "${CLOJURE_ALIASES:?CLOJURE_ALIASES is required}"
|
||||
: "${TEST_RUNNER_INDEX:?TEST_RUNNER_INDEX is required}"
|
||||
: "${TEST_RUNNER_COUNT:?TEST_RUNNER_COUNT is required}"
|
||||
TEST_ARGS=("$@")
|
||||
TEST_JVMS_PER_RUNNER="${TEST_JVMS_PER_RUNNER:-1}"
|
||||
TEST_HEAP_PER_JVM="${TEST_HEAP_PER_JVM:-4g}"
|
||||
|
||||
die() {
|
||||
echo "::error::run-backend-test-jvms.sh: $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$TEST_RUNNER_INDEX" =~ ^[0-9]+$ ]] || die "TEST_RUNNER_INDEX must be a non-negative integer, got '$TEST_RUNNER_INDEX'"
|
||||
[[ "$TEST_RUNNER_COUNT" =~ ^[0-9]+$ ]] || die "TEST_RUNNER_COUNT must be a positive integer, got '$TEST_RUNNER_COUNT'"
|
||||
[[ "$TEST_JVMS_PER_RUNNER" =~ ^[0-9]+$ ]] || die "TEST_JVMS_PER_RUNNER must be a positive integer, got '$TEST_JVMS_PER_RUNNER'"
|
||||
[ "$TEST_RUNNER_COUNT" -ge 1 ] || die "TEST_RUNNER_COUNT must be at least 1"
|
||||
[ "$TEST_JVMS_PER_RUNNER" -ge 1 ] || die "TEST_JVMS_PER_RUNNER must be at least 1"
|
||||
[ "$TEST_RUNNER_INDEX" -lt "$TEST_RUNNER_COUNT" ] ||
|
||||
die "runner index $TEST_RUNNER_INDEX is out of range for $TEST_RUNNER_COUNT runner(s) — a matrix entry was added without raising runner-count"
|
||||
|
||||
# Partition indexes are split across runners rather than handed out in contiguous
|
||||
# blocks. E.g. with 2 runners and 2 JVMs the first runner gets partition 0 and 2, and
|
||||
# the second runner gets partitions 1 and 3. Hawk partitions in namespace order, so
|
||||
# neighbouring partitions have similar runtimes — contiguous blocks would group the slow
|
||||
# ones together onto one runner.
|
||||
PARTITION_TOTAL=$((TEST_RUNNER_COUNT * TEST_JVMS_PER_RUNNER))
|
||||
INDEXES=()
|
||||
for ((k = 0; k < TEST_JVMS_PER_RUNNER; k++)); do
|
||||
INDEXES+=($((TEST_RUNNER_INDEX + k * TEST_RUNNER_COUNT)))
|
||||
done
|
||||
|
||||
# One JVM: run directly in the workspace with no heap override, exactly as a
|
||||
# partitioned run behaved before this script existed.
|
||||
if [ "$TEST_JVMS_PER_RUNNER" -eq 1 ]; then
|
||||
echo "Running partition ${INDEXES[0]} of $PARTITION_TOTAL"
|
||||
exec clojure -X:"$CLOJURE_ALIASES" "${TEST_ARGS[@]}" \
|
||||
:partition/total "$PARTITION_TOTAL" \
|
||||
:partition/index "${INDEXES[0]}"
|
||||
fi
|
||||
|
||||
|
||||
WORKSPACE="$PWD"
|
||||
# Sibling of the checkout: same filesystem (so `cp -al` can hardlink) but outside the
|
||||
# repo, so one JVM's directory never gets copied into another's.
|
||||
JVM_DIRS_ROOT="$(dirname "$WORKSPACE")/backend-test-jvms"
|
||||
STATUS_DIR="$JVM_DIRS_ROOT/status"
|
||||
|
||||
# Merge every JVM's results into the workspace the rest of the job reads from.
|
||||
#
|
||||
# Each partition's junit XML goes into its own subdirectory: `mb_hawk_var_less_errors.xml`
|
||||
# has a fixed name, so a flat copy would silently overwrite it. Nesting is safe because
|
||||
# both consumers (dorny/test-reporter and ci-conductor's backend adapter) walk
|
||||
# `target/junit` recursively and match on filename suffix.
|
||||
#
|
||||
# Runs from an EXIT trap so a cancelled or timed-out job still publishes whatever
|
||||
# finished.
|
||||
# shellcheck disable=SC2329 # invoked by the EXIT trap below, which shellcheck can't see.
|
||||
merge_outputs() {
|
||||
mkdir -p "$WORKSPACE/target/junit" "$WORKSPACE/logs"
|
||||
local idx jvm_dir
|
||||
for idx in "${INDEXES[@]}"; do
|
||||
jvm_dir="$JVM_DIRS_ROOT/partition-$idx"
|
||||
if [ -d "$jvm_dir/target/junit" ]; then
|
||||
mkdir -p "$WORKSPACE/target/junit/p$idx"
|
||||
cp -a "$jvm_dir/target/junit/." "$WORKSPACE/target/junit/p$idx/" || true
|
||||
fi
|
||||
# `logs/test-log*.json` is what the action uploads on failure.
|
||||
if [ -f "$jvm_dir/logs/test-log.json" ]; then
|
||||
cp -a "$jvm_dir/logs/test-log.json" "$WORKSPACE/logs/test-log-p$idx.json" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
trap merge_outputs EXIT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
|
||||
# Hardlinked copy of the checkout. Skips the paths a run writes to (`target`, `logs`, `.cpcache`)
|
||||
# and the two most expensive trees nothing on the test classpath reads (`.git`, `node_modules`).
|
||||
prepare_jvm_dir() {
|
||||
local jvm_dir="$1" entry
|
||||
rm -rf "$jvm_dir"
|
||||
mkdir -p "$jvm_dir"
|
||||
for entry in "$WORKSPACE"/* "$WORKSPACE"/.[!.]*; do
|
||||
[ -e "$entry" ] || continue
|
||||
case "$(basename "$entry")" in
|
||||
.git | node_modules | target | logs | .cpcache) continue ;;
|
||||
esac
|
||||
# Fall back to a real copy if the filesystem refuses the hardlink.
|
||||
cp -al "$entry" "$jvm_dir/" 2>/dev/null || cp -a "$entry" "$jvm_dir/"
|
||||
done
|
||||
}
|
||||
|
||||
# Prefix output with the partition index so interleaved JVM logs stay readable.
|
||||
run_partition() {
|
||||
local idx="$1" jvm_dir="$2" rc line
|
||||
(
|
||||
cd "$jvm_dir" || exit 1
|
||||
# `-J` options land after the aliases' `:jvm-opts`, overriding the `:ci` alias's
|
||||
# `-Xms12g -Xmx12g`. Xms stays small so JVMs sharing a runner grow into memory on
|
||||
# demand rather than each committing its ceiling up front.
|
||||
exec clojure -J-Xms512m -J-Xmx"$TEST_HEAP_PER_JVM" -X:"$CLOJURE_ALIASES" \
|
||||
"${TEST_ARGS[@]}" \
|
||||
:partition/total "$PARTITION_TOTAL" \
|
||||
:partition/index "$idx"
|
||||
) 2>&1 | while IFS= read -r line || [ -n "$line" ]; do printf '[p%s] %s\n' "$idx" "$line"; done
|
||||
rc="${PIPESTATUS[0]}"
|
||||
echo "$rc" > "$STATUS_DIR/p$idx"
|
||||
}
|
||||
|
||||
rm -rf "$JVM_DIRS_ROOT"
|
||||
mkdir -p "$STATUS_DIR"
|
||||
|
||||
echo "Runner $TEST_RUNNER_INDEX of $TEST_RUNNER_COUNT: running $TEST_JVMS_PER_RUNNER concurrent JVMs for partition(s) ${INDEXES[*]} of $PARTITION_TOTAL (-Xmx$TEST_HEAP_PER_JVM each)"
|
||||
|
||||
for idx in "${INDEXES[@]}"; do
|
||||
prepare_jvm_dir "$JVM_DIRS_ROOT/partition-$idx"
|
||||
done
|
||||
|
||||
for idx in "${INDEXES[@]}"; do
|
||||
run_partition "$idx" "$JVM_DIRS_ROOT/partition-$idx" &
|
||||
done
|
||||
wait
|
||||
|
||||
overall=0
|
||||
for idx in "${INDEXES[@]}"; do
|
||||
rc="$(cat "$STATUS_DIR/p$idx" 2>/dev/null || echo 1)"
|
||||
echo "partition $idx exited $rc"
|
||||
[ "$rc" -eq 0 ] || overall=1
|
||||
done
|
||||
exit "$overall"
|
||||
97
.github/scripts/upload-bundle-load-stats.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
type LoadTimeRow = Record<string, string | number>;
|
||||
|
||||
function buildRows(
|
||||
conditions: Condition[],
|
||||
{ sha, subject }: CommitStamp,
|
||||
): LoadTimeRow[] {
|
||||
return conditions.map((condition) => ({
|
||||
Date: new Date().toISOString().slice(0, 10),
|
||||
// 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 || "",
|
||||
});
|
||||
|
||||
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:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
name: Block Master Branch PRs
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-source-branch:
|
||||
if: github.head_ref == 'master'
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Block master branch PRs
|
||||
run: |
|
||||
echo "PRs from \`master\` branches are not allowed, please use a new branch name for your PR."
|
||||
exit 1
|
||||
43
.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
|
||||
@@ -108,7 +137,7 @@ jobs:
|
||||
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 }}
|
||||
|
||||
21
.github/workflows/check-backport-script.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: Prevent backport.sh from being merged
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
check-backport-script:
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Check for backport.sh file
|
||||
run: |
|
||||
if [ -f "./backport.sh" ]; then
|
||||
echo "backport.sh file exists. Please remove it before merging."
|
||||
exit 1
|
||||
else
|
||||
echo "All good."
|
||||
fi
|
||||
80
.github/workflows/containerize-jar.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Containerize Uberjar
|
||||
run-name: Containerize ${{ inputs.tag }} Uberjar
|
||||
run-name: Containerize ${{ inputs.edition }} ${{ inputs.tag }} Uberjar
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -9,9 +9,10 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
repo:
|
||||
description: owner and repo like "metabase/metabase"
|
||||
description: Docker owner and repo, e.g. "metabase/metabase".
|
||||
type: string
|
||||
tag:
|
||||
description: Docker tag, e.g. v1.87.2.
|
||||
type: string
|
||||
commit:
|
||||
type: string
|
||||
@@ -30,10 +31,10 @@ on:
|
||||
- oss
|
||||
required: true
|
||||
repo:
|
||||
description: docker owner and repo like "metabase/metabase"
|
||||
description: Docker owner and repo, e.g. "metabase/metabase".
|
||||
type: string
|
||||
tag:
|
||||
description: usually a version like v1.87.2
|
||||
description: Docker tag, e.g. v1.87.2.
|
||||
type: string
|
||||
commit:
|
||||
description: the full commit hash
|
||||
@@ -45,10 +46,14 @@ on:
|
||||
|
||||
jobs:
|
||||
containerize:
|
||||
name: containerize (${{ inputs.edition }})
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
platforms: ${{ steps.platforms.outputs.result }}
|
||||
repo: ${{ steps.image.outputs.repo }}
|
||||
tag: ${{ steps.image.outputs.tag }}
|
||||
full_image: ${{ steps.image.outputs.full_image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -65,6 +70,13 @@ jobs:
|
||||
release: ${{ inputs.release }}
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
- name: Resolve image repo and tag
|
||||
id: image
|
||||
uses: ./.github/actions/get-docker-info
|
||||
with:
|
||||
repo: ${{ inputs.repo }}
|
||||
tag: ${{ inputs.tag }}
|
||||
edition: ${{ inputs.edition }}
|
||||
- name: Get platforms
|
||||
id: platforms
|
||||
uses: actions/github-script@v9
|
||||
@@ -72,7 +84,7 @@ jobs:
|
||||
result-encoding: string
|
||||
script: | # js
|
||||
const { getBuildRequirements, isValidVersionString} = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const version = "${{ inputs.tag }}".split('-')[0];
|
||||
const version = "${{ steps.image.outputs.tag }}".split('-')[0];
|
||||
|
||||
if (!isValidVersionString(version)) {
|
||||
return "linux/amd64,linux/arm64";
|
||||
@@ -100,7 +112,7 @@ jobs:
|
||||
context: bin/docker/.
|
||||
platforms: ${{ steps.platforms.outputs.result }}
|
||||
network: host
|
||||
tags: ${{ inputs.repo }}:${{ inputs.tag }}
|
||||
tags: ${{ steps.image.outputs.full_image }}
|
||||
no-cache: true
|
||||
push: true
|
||||
build-args: ${{ inputs.build-args }}
|
||||
@@ -108,16 +120,26 @@ jobs:
|
||||
# we need to do this to get the nice fallback behavior of the legacy manifest file
|
||||
- name: Build container (x86 only)
|
||||
if: contains(steps.platforms.outputs.result, 'arm') == false
|
||||
env:
|
||||
BUILD_ARGS: ${{ inputs.build-args }}
|
||||
FULL_IMAGE: ${{ steps.image.outputs.full_image }}
|
||||
run: | # bash
|
||||
args=()
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && args+=(--build-arg "$line")
|
||||
done <<< "$BUILD_ARGS"
|
||||
|
||||
docker build \
|
||||
-t ${{ inputs.repo }}:${{ inputs.tag }} \
|
||||
-t "$FULL_IMAGE" \
|
||||
"${args[@]}" \
|
||||
--network=host \
|
||||
--push \
|
||||
bin/docker/.
|
||||
|
||||
verify-docker-pull:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
needs: containerize
|
||||
name: verify-docker-pull (${{ inputs.edition }})
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
@@ -126,18 +148,19 @@ jobs:
|
||||
username: ${{ inputs.release && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ inputs.release && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Pull the container image
|
||||
run: docker pull ${{ inputs.repo }}:${{ inputs.tag }}
|
||||
run: docker pull ${{ needs.containerize.outputs.full_image }}
|
||||
- name: Launch container
|
||||
run: docker run --rm -dp 3000:3000 ${{ inputs.repo }}:${{ inputs.tag }}
|
||||
run: docker run --rm -dp 3000:3000 ${{ needs.containerize.outputs.full_image }}
|
||||
timeout-minutes: 5
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
timeout-minutes: 5
|
||||
|
||||
verify-docker-pull-arm:
|
||||
runs-on: arm-ubuntu-24-2core
|
||||
needs: containerize
|
||||
if: contains(needs.containerize.outputs.platforms, 'linux/arm64')
|
||||
name: verify-docker-pull-arm (${{ inputs.edition }})
|
||||
runs-on: arm-ubuntu-24-2core
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
@@ -146,10 +169,41 @@ jobs:
|
||||
username: ${{ inputs.release && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ inputs.release && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Pull the container image
|
||||
run: docker pull ${{ inputs.repo }}:${{ inputs.tag }}
|
||||
run: docker pull ${{ needs.containerize.outputs.full_image }}
|
||||
- name: Launch container
|
||||
run: docker run --rm -dp 3000:3000 ${{ inputs.repo }}:${{ inputs.tag }}
|
||||
run: docker run --rm -dp 3000:3000 ${{ needs.containerize.outputs.full_image }}
|
||||
timeout-minutes: 5
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
timeout-minutes: 5
|
||||
|
||||
post-docker-info-comment:
|
||||
needs: [containerize, verify-docker-pull, verify-docker-pull-arm]
|
||||
# Only post a comment if all the steps pass and this is a PR.
|
||||
if: |
|
||||
!cancelled() &&
|
||||
!failure() &&
|
||||
!inputs.release &&
|
||||
github.event_name == 'pull_request'
|
||||
name: post-docker-info-comment (${{ inputs.edition }})
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Post PR comment
|
||||
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
|
||||
with:
|
||||
header: docker-build-status-${{ inputs.edition }}
|
||||
message: |
|
||||
✅ Docker build completed for commit ${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
The uberjar has been built and containerized successfully.
|
||||
|
||||
**Branch:** `${{ github.head_ref || github.ref_name }}`
|
||||
**Docker Image:** `${{ needs.containerize.outputs.full_image }}`
|
||||
**Docker Hub:** [View on Docker Hub](https://hub.docker.com/r/${{ needs.containerize.outputs.repo }}/tags?name=${{ needs.containerize.outputs.tag }})
|
||||
**Triggered by:** @${{ github.actor }}
|
||||
|
||||
Pull the image:
|
||||
```bash
|
||||
docker pull ${{ needs.containerize.outputs.full_image }}
|
||||
```
|
||||
|
||||
102
.github/workflows/containerize-uberjar.yml
vendored
@@ -1,102 +0,0 @@
|
||||
name: Containerize Uberjar
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit:
|
||||
description: the full commit hash to containerize (defaults to the triggering commit)
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
get-container-info:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
name: Get container info
|
||||
outputs:
|
||||
editions: ${{ steps.ee-info.outputs.editions }}
|
||||
ee: ${{ toJson(fromJson(steps.build-matrix.outputs.result).ee) }}
|
||||
oss: ${{ toJson(fromJson(steps.build-matrix.outputs.result).oss) }}
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
- name: Get EE Docker info
|
||||
id: ee-info
|
||||
uses: ./.github/actions/get-docker-info
|
||||
with:
|
||||
edition: ee
|
||||
- name: Get OSS Docker info
|
||||
id: oss-info
|
||||
uses: ./.github/actions/get-docker-info
|
||||
with:
|
||||
edition: oss
|
||||
- name: Build matrix info
|
||||
id: build-matrix
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: | # js
|
||||
const editions = JSON.parse('${{ steps.ee-info.outputs.editions }}');
|
||||
const result = {
|
||||
editions: editions,
|
||||
ee: {
|
||||
repo: '${{ steps.ee-info.outputs.repo }}',
|
||||
tag: '${{ steps.ee-info.outputs.tag }}'
|
||||
},
|
||||
oss: {
|
||||
repo: '${{ steps.oss-info.outputs.repo }}',
|
||||
tag: '${{ steps.oss-info.outputs.tag }}'
|
||||
}
|
||||
};
|
||||
console.log('Build matrix:', result);
|
||||
return result;
|
||||
|
||||
containerize:
|
||||
needs: [get-container-info]
|
||||
if: needs.get-container-info.result == 'success'
|
||||
strategy:
|
||||
matrix:
|
||||
edition: ${{ fromJson(needs.get-container-info.outputs.editions) }}
|
||||
uses: ./.github/workflows/containerize-jar.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
edition: ${{ matrix.edition }}
|
||||
commit: ${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
repo: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).repo }}
|
||||
tag: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).tag }}
|
||||
release: false
|
||||
build-args: |
|
||||
GIT_COMMIT_SHA=${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
post-comment:
|
||||
needs: [get-container-info, containerize]
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
name: Post PR comment
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
- name: Get Docker info for comment
|
||||
id: docker-info
|
||||
uses: ./.github/actions/get-docker-info
|
||||
with:
|
||||
branch: ${{ github.head_ref || github.ref_name }}
|
||||
edition: ee
|
||||
|
||||
- name: Post PR comment
|
||||
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
|
||||
with:
|
||||
header: docker-build-status
|
||||
message: |
|
||||
✅ Docker build completed for commit ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
The uberjar has been built and containerized successfully.
|
||||
|
||||
**Branch:** `${{ github.head_ref || github.ref_name }}`
|
||||
**Docker Image:** `${{ steps.docker-info.outputs.full_image }}`
|
||||
**Docker Hub:** [View on Docker Hub](https://hub.docker.com/r/${{ steps.docker-info.outputs.repo }}/tags?name=${{ steps.docker-info.outputs.tag }})
|
||||
**Triggered by:** @${{ github.actor }}
|
||||
|
||||
Pull the image:
|
||||
```bash
|
||||
docker pull ${{ steps.docker-info.outputs.full_image }}
|
||||
```
|
||||
3
.github/workflows/docs-generate-base.yml
vendored
@@ -77,6 +77,9 @@ jobs:
|
||||
|
||||
- name: Generate AI provider documentation
|
||||
id: generate_ai_provider_docs
|
||||
# The generator only exists on branches that ship the multi-provider registry (master / 64+), but
|
||||
# this reusable workflow always runs from master, so skip the step on release branches without it.
|
||||
if: hashFiles('src/metabase/cmd/ai_provider_dox.clj') != ''
|
||||
run: |
|
||||
set -e # Exit on any error
|
||||
echo "Generating AI provider documentation..."
|
||||
|
||||
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
|
||||
|
||||
90
.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,35 +163,16 @@ 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
|
||||
if: ${{ needs.determine-driver-skips.outputs.bigquery-should-run == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 70
|
||||
timeout-minutes: 40
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- name: Driver Tests Part 1
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: Driver Tests Part 2
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
- name: Transforms Python Tests
|
||||
test-args: >-
|
||||
:only-tags [:mb/transforms-python-test]
|
||||
needs-python-runner: true
|
||||
runner-index: [0, 1]
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: bigquery-cloud-sdk
|
||||
@@ -201,11 +183,43 @@ jobs:
|
||||
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
|
||||
name: BigQuery ${{ matrix.job.name }}
|
||||
name: BigQuery Driver Tests ${{ matrix.runner-index }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Test BigQuery Cloud SDK driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
runner-index: ${{ matrix.runner-index }}
|
||||
runner-count: ${{ strategy.job-total }}
|
||||
jvms-per-runner: '2'
|
||||
heap-per-jvm: '4g'
|
||||
artifact-suffix: runner-${{ matrix.runner-index }}
|
||||
|
||||
be-tests-bigquery-transforms-python-ee:
|
||||
needs: determine-driver-skips
|
||||
if: ${{ needs.determine-driver-skips.outputs.bigquery-should-run == 'true' }}
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: bigquery-cloud-sdk
|
||||
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
|
||||
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
|
||||
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
|
||||
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
|
||||
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
|
||||
name: BigQuery Transforms Python Tests
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup python-runner
|
||||
if: ${{ matrix.job.needs-python-runner == true }}
|
||||
uses: ./.github/actions/setup-python-runner
|
||||
with:
|
||||
metabase-automation-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
@@ -213,16 +227,17 @@ jobs:
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
# This is the same as the cloud-sdk job above to preserve quarantine history in ci-conductor.
|
||||
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
test-args: >-
|
||||
:only-tags [:mb/transforms-python-test]
|
||||
artifact-suffix: transforms-python
|
||||
- name: Cleanup python-runner
|
||||
if: ${{ always() && matrix.job.needs-python-runner == true }}
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/actions/cleanup-python-runner
|
||||
with:
|
||||
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
|
||||
logs-artifact-suffix: ${{ matrix.job.name }}
|
||||
logs-artifact-suffix: transforms-python
|
||||
|
||||
be-tests-clickhouse-ee:
|
||||
needs: determine-driver-skips
|
||||
@@ -279,7 +294,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 +327,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 +389,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 +408,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 +428,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 +546,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 +605,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 +666,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 +690,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 +754,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 +846,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 +912,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 +1002,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 +1064,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 +1095,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 +1115,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 +1170,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 +1266,6 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
|
||||
|
||||
drivers-tests-result:
|
||||
needs:
|
||||
@@ -1276,6 +1273,7 @@ jobs:
|
||||
# if you add a driver job, you must add it to this list for it to be a required check
|
||||
- be-tests-athena-ee
|
||||
- be-tests-bigquery-cloud-sdk-ee
|
||||
- be-tests-bigquery-transforms-python-ee
|
||||
- be-tests-clickhouse-ee
|
||||
- be-tests-databricks-ee
|
||||
- be-tests-druid-jdbc-ee
|
||||
|
||||
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 }}
|
||||
@@ -438,15 +424,38 @@ jobs:
|
||||
if: steps.prebuilt-cljs.outcome != 'success'
|
||||
run: bun run build-pure:cljs
|
||||
|
||||
# The data-app suite synchronizes real actions, and Metabase only permits
|
||||
# those against a writable warehouse, which comes from the `qa-db`
|
||||
# snapshot below. That snapshot creator builds every QA database in one
|
||||
# pass — Mongo and MySQL included — so all three have to be up even though
|
||||
# only `postgres-writable` is restored. The other host apps read the sample
|
||||
# database, so they skip the pull entirely.
|
||||
- name: Prepare Docker containers
|
||||
if: ${{ matrix.test_suite == 'vite-6-data-app-host-app-e2e' }}
|
||||
uses: ./.github/actions/e2e-prepare-containers
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
postgres: true
|
||||
mysql: true
|
||||
mongo: true
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- uses: ./.github/actions/resolve-job-id
|
||||
|
||||
# Without `--spec` the snapshot config picks up `qa-db.cy.snap.js` too,
|
||||
# which is what creates the `postgres-writable` snapshot the data-app
|
||||
# action tests restore.
|
||||
- name: Generate database snapshots
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
if [ "${{ matrix.test_suite }}" = "vite-6-data-app-host-app-e2e" ]; then
|
||||
node e2e/runner/run_cypress_ci.js snapshot
|
||||
else
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
fi
|
||||
|
||||
- name: Prepare and launch Host App
|
||||
run: npx tsx ./e2e/runner/embedding-sdk/host-apps/start-ci.ts ${{ matrix.test_suite }}
|
||||
|
||||
@@ -24,6 +24,8 @@ jobs:
|
||||
# unrelated to embedding (EMB-765)
|
||||
echo "::remove-matcher owner=tsc::"
|
||||
bun run build-embedding-sdk-package
|
||||
- name: Smoke test the packed SDK CLI
|
||||
run: node bin/embedding-sdk/test-sdk-package.mjs
|
||||
- name: Prepare Embedding SDK artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
30
.github/workflows/kondo-ratchets-shrink.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Shrink ratchets
|
||||
|
||||
# Lowers the budgets in .clj-kondo/ratchets.edn to match master and delivers the result through one
|
||||
# Lowers the ratchet budgets to match master and delivers the result through one
|
||||
# automation PR, because the rulesets require a pull request for every change to master. The PR is
|
||||
# approved by github-actions[bot] and merges itself once the ratchet check passes. A merge of that PR
|
||||
# touches only ratchets.edn, which matches `.clj-kondo/**` below, so it triggers one more run that finds
|
||||
# touches only ratchet files, which match `.clj-kondo/**` below, so it triggers one more run that finds
|
||||
# nothing to lower and stops: a single no-op, not a loop.
|
||||
|
||||
on:
|
||||
@@ -32,7 +32,8 @@ concurrency:
|
||||
|
||||
env:
|
||||
AUTOMATION_BRANCH: automation/shrink-ratchets
|
||||
RATCHETS_FILE: .clj-kondo/ratchets.edn
|
||||
KONDO_RATCHETS_FILE: .clj-kondo/ratchets.edn
|
||||
MODULE_RATCHETS_FILE: .clj-kondo/config/modules/ratchets.edn
|
||||
|
||||
jobs:
|
||||
shrink:
|
||||
@@ -56,9 +57,10 @@ jobs:
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
- name: Shrink ignore limits
|
||||
- name: Shrink ratchets
|
||||
run: |
|
||||
cp "$RATCHETS_FILE" "$RUNNER_TEMP/ratchets-before.edn"
|
||||
cp "$KONDO_RATCHETS_FILE" "$RUNNER_TEMP/kondo-ratchets-before.edn"
|
||||
cp "$MODULE_RATCHETS_FILE" "$RUNNER_TEMP/module-ratchets-before.edn"
|
||||
./bin/mage kondo-ratchets-shrink
|
||||
# Two independent outputs: `push` says the branch needs a new commit, `pr` names the open automation
|
||||
# PR if there is one. Approval and auto-merge key on `pr` alone, so a run that failed after opening
|
||||
@@ -77,23 +79,23 @@ jobs:
|
||||
--jq '.[0].number // empty')
|
||||
echo "pr=$open_pr" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if git diff --quiet -- "$RATCHETS_FILE"; then
|
||||
if git diff --quiet -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE"; then
|
||||
echo "Ratchet budgets already match master $(git rev-parse --short HEAD)"
|
||||
[ -z "$open_pr" ] || echo "The open automation PR #$open_pr is re-approved below"
|
||||
echo "push=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The ratchet file is the whole payload, so an open PR that already carries this result is left
|
||||
# The ratchet files are the whole payload, so an open PR that already carries this result is left
|
||||
# alone. Rebasing it onto every master push would restart its CI and churn the diff without
|
||||
# changing what it delivers. Compare the open PR with the working tree, which contains the
|
||||
# shrunk ratchets file produced above, to determine whether the PR would deliver the same file.
|
||||
# shrunk ratchet files produced above, to determine whether the PR would deliver the same result.
|
||||
#
|
||||
# A conflicted PR is the exception: only a fresh commit clears it. Mergeability is computed
|
||||
# lazily, so a pending UNKNOWN reads as not conflicted and the next run settles it.
|
||||
if [ -n "$open_pr" ] \
|
||||
&& git fetch --quiet --depth=1 origin "refs/heads/$AUTOMATION_BRANCH" 2>/dev/null \
|
||||
&& git diff --quiet FETCH_HEAD -- "$RATCHETS_FILE" \
|
||||
&& git diff --quiet FETCH_HEAD -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE" \
|
||||
&& [ "$(gh pr view "$open_pr" --repo "$GITHUB_REPOSITORY" \
|
||||
--json mergeable --jq .mergeable)" != CONFLICTING ]; then
|
||||
echo "The automation PR #$open_pr already carries this result"
|
||||
@@ -102,7 +104,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git diff --stat -- "$RATCHETS_FILE"
|
||||
git diff --stat -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE"
|
||||
echo "push=true" >> "$GITHUB_OUTPUT"
|
||||
- name: Update the automation PR
|
||||
id: deliver
|
||||
@@ -116,14 +118,16 @@ jobs:
|
||||
OPEN_PR: ${{ steps.compare.outputs.pr }}
|
||||
run: |
|
||||
./bin/mage -kondo-ratchets-pr-body \
|
||||
"$RUNNER_TEMP/ratchets-before.edn" \
|
||||
"$RATCHETS_FILE" \
|
||||
"$RUNNER_TEMP/kondo-ratchets-before.edn" \
|
||||
"$KONDO_RATCHETS_FILE" \
|
||||
"$RUNNER_TEMP/module-ratchets-before.edn" \
|
||||
"$MODULE_RATCHETS_FILE" \
|
||||
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
> "$RUNNER_TEMP/ratchets-pr-body.md"
|
||||
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git add -- "$RATCHETS_FILE"
|
||||
git add -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE"
|
||||
git commit --quiet -m "Tighten ratchets"
|
||||
git push --force --quiet \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
|
||||
|
||||
25
.github/workflows/linear-pr-links.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Add Issue References to PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
add-issue-references:
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Run the link script
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
with:
|
||||
script: |
|
||||
const { link_issues } = require('${{ github.workspace }}/.github/scripts/update-pr-with-issues.js');
|
||||
await link_issues(github)
|
||||
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 }}
|
||||
52
.github/workflows/pr.checks.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# Helpful (and quick) checks that run on PRs.
|
||||
|
||||
name: PR Checks
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
/.github/scripts/update-pr-with-issues.js
|
||||
/backport.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
# download repo structure without file contents, fetched on demand since we only need a few files
|
||||
# NOTE: this gets slow if we start accessing more files, remove this filter in that case.
|
||||
filter: blob:none
|
||||
|
||||
- parallel:
|
||||
- name: Block master branch PRs (from forks)
|
||||
if: github.head_ref == 'master'
|
||||
run: |
|
||||
echo "PRs from \`master\` branches are not allowed, please use a new branch name for your PR."
|
||||
exit 1
|
||||
|
||||
- name: Link PR and issues
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
with:
|
||||
script: |
|
||||
const { link_issues } = require('${{ github.workspace }}/.github/scripts/update-pr-with-issues.js');
|
||||
await link_issues(github)
|
||||
|
||||
- name: Prevent backport.sh from being merged
|
||||
run: |
|
||||
if [ -f "./backport.sh" ]; then
|
||||
echo "backport.sh file exists. Please remove it before merging."
|
||||
exit 1
|
||||
else
|
||||
echo "All good."
|
||||
fi
|
||||
45
.github/workflows/pull-request-reminders.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Pull Request Reminders
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
branches:
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
external-reminder:
|
||||
name: External PR Helper
|
||||
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: read
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: | # js
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const pullNumber = '${{ github.event.pull_request.number }}';
|
||||
const author = '${{ github.event.pull_request.user.login }}';
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
labels: ["External"],
|
||||
});
|
||||
|
||||
const guideLink = "https://www.notion.so/metabase/Third-Party-PRs-External-contributions-5954061854424c1ea6391d98746646e0";
|
||||
const message = `@${author} Thanks for your contribution! ❤️ We'll try to review it as soon as we can.\n\n
|
||||
Reviewers, make sure to check out our [External PR Review Guide](${guideLink}) when reviewing.
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
body: message,
|
||||
});
|
||||
3
.github/workflows/release-embedding-sdk.yml
vendored
@@ -382,6 +382,9 @@ jobs:
|
||||
- name: Generate SDK package.json in the build directory
|
||||
run: bun run embedding-sdk:generate-package
|
||||
|
||||
- name: Smoke test the packed SDK CLI
|
||||
run: node bin/embedding-sdk/test-sdk-package.mjs
|
||||
|
||||
- name: Upload built SDK package as artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
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"
|
||||
14
.github/workflows/run-tests.yml
vendored
@@ -148,6 +148,8 @@ jobs:
|
||||
(needs.should-run.outputs.verdict == 'force-run' ||
|
||||
needs.files-changed.outputs.no_source_code_changes != 'true')
|
||||
uses: ./.github/workflows/uberjar.yml
|
||||
with:
|
||||
containerize: ${{ (github.event_name == 'push' && github.ref_name == 'master') || contains(github.event.pull_request.labels.*.name, 'build-docker-uberjar') }}
|
||||
secrets: inherit
|
||||
|
||||
# Compile CLJS in parallel with the uberjar and pass it to each e2e group.
|
||||
@@ -414,15 +416,3 @@ jobs:
|
||||
self_hosting: false
|
||||
analytics_dev_mode: false
|
||||
stats_dump: true
|
||||
|
||||
containerize:
|
||||
needs: [uberjar]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.uberjar.result == 'success' &&
|
||||
(
|
||||
(github.event_name == 'push' && contains(fromJson('["master", "metabot-v3-main", "internal-tools"]'), github.ref_name)) ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'build-docker-uberjar'))
|
||||
)
|
||||
uses: ./.github/workflows/containerize-uberjar.yml
|
||||
secrets: inherit
|
||||
|
||||
2
.github/workflows/schedule-patch.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
// Nightly patches cover the current supported major and the one
|
||||
// before it. release-auto.yml releases one major per dispatch.
|
||||
const currentRelease = Number('${{ vars.CURRENT_VERSION }}');
|
||||
const majors = [currentRelease - 1, currentRelease];
|
||||
const majors = [currentRelease];
|
||||
|
||||
await Promise.all(majors.map(version =>
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
|
||||
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 }}
|
||||
125
.github/workflows/test.bundle-load-stats.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
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 }}
|
||||
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.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Uberjar Containerize labeled
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ labeled, opened, synchronize, reopened ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
uberjar:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'build-docker-uberjar') || github.event.label.name == 'build-docker-uberjar' }}
|
||||
uses: ./.github/workflows/uberjar.yml
|
||||
secrets: inherit
|
||||
|
||||
containerize:
|
||||
needs: [uberjar]
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'build-docker-uberjar') || github.event.label.name == 'build-docker-uberjar' }}
|
||||
uses: ./.github/workflows/containerize-uberjar.yml
|
||||
secrets: inherit
|
||||
173
.github/workflows/uberjar.yml
vendored
@@ -1,108 +1,89 @@
|
||||
name: Build Uberjar
|
||||
|
||||
# This workflow builds an uberjar and pushes it to dockerhub for any branch, either on-demand,
|
||||
# or automatically for the listed branches
|
||||
# This workflow builds and containerizes an uberjar and pushes it to dockerhub.
|
||||
|
||||
concurrency:
|
||||
group: uberjar-${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- internal-tools
|
||||
- ai-service-port
|
||||
- metabot-mbql-improvements
|
||||
paths-ignore:
|
||||
# config files
|
||||
- ".**"
|
||||
# documentation
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
# this covers both BE and FE unit tests, as well as E2E tests
|
||||
- "**test/**"
|
||||
- "**_test.clj"
|
||||
- "**/frontend/**.unit.*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit:
|
||||
description: "Optional full-length commit SHA-1 hash"
|
||||
type: string
|
||||
default: ''
|
||||
required: false
|
||||
containerize:
|
||||
description: "Set if the uberjar should be containerized and pushed to Dockerhub."
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
description: "Optional full-length commit SHA-1 hash"
|
||||
type: string
|
||||
default: ''
|
||||
required: false
|
||||
containerize:
|
||||
description: "Set if the uberjar should be containerized and pushed to Dockerhub."
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
check-existing-artifacts:
|
||||
setup:
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
ee_exists: ${{ steps.check-ee.outcome == 'success' }}
|
||||
oss_exists: ${{ steps.check-oss.outcome == 'success' }}
|
||||
should_build: ${{ steps.decide.outputs.should_build }}
|
||||
build_editions: ${{ steps.matrix.outputs.build_editions }}
|
||||
java_version: ${{ steps.dependencies.outputs.java_version }}
|
||||
node_version: ${{ steps.dependencies.outputs.node_version }}
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
- name: Try to fetch existing EE artifact
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check for existing EE uberjar
|
||||
id: check-ee
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
continue-on-error: true
|
||||
with:
|
||||
commit: ${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
edition: ee
|
||||
output-name: metabase-ee.jar
|
||||
download: false
|
||||
|
||||
- name: Try to fetch existing OSS artifact
|
||||
- name: Check for existing OSS uberjar
|
||||
id: check-oss
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
continue-on-error: true
|
||||
with:
|
||||
commit: ${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
edition: oss
|
||||
output-name: metabase-oss.jar
|
||||
download: false
|
||||
|
||||
- name: Decide if build is needed
|
||||
id: decide
|
||||
run: |
|
||||
EE_EXISTS="${{ steps.check-ee.outcome == 'success' }}"
|
||||
OSS_EXISTS="${{ steps.check-oss.outcome == 'success' }}"
|
||||
|
||||
echo "EE exists: $EE_EXISTS"
|
||||
echo "OSS exists: $OSS_EXISTS"
|
||||
|
||||
# Build only if artifacts don't exist for this commit hash
|
||||
# Since we build by hash, if artifacts exist for this hash, we don't need to rebuild
|
||||
if [[ "$EE_EXISTS" == "false" ]] || [[ "$OSS_EXISTS" == "false" ]]; then
|
||||
SHOULD_BUILD="true"
|
||||
echo "Will build: ee_exists=$EE_EXISTS, oss_exists=$OSS_EXISTS"
|
||||
else
|
||||
SHOULD_BUILD="false"
|
||||
echo "Skipping build: artifacts exist for this commit hash"
|
||||
fi
|
||||
|
||||
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# if this is on a release branch, we need to check the build requirements
|
||||
get-build-requirements:
|
||||
if: |
|
||||
!cancelled() &&
|
||||
contains(github.ref, 'release-x')
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
|
||||
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Create build matrix
|
||||
id: matrix
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
sparse-checkout: |
|
||||
release
|
||||
.github
|
||||
script: | # js
|
||||
const hit = {
|
||||
ee: "${{ steps.check-ee.outputs.fetch-hit }}" === "true",
|
||||
oss: "${{ steps.check-oss.outputs.fetch-hit }}" === "true",
|
||||
};
|
||||
|
||||
// Don't rebuild an edition whose uberjar already exists for this commit.
|
||||
const editions = ['ee', 'oss'].filter((edition) => !hit[edition]);
|
||||
|
||||
core.info(`build: ${editions.join(', ') || '(nothing, both artifacts already exist)'}`);
|
||||
|
||||
core.setOutput('build_editions', JSON.stringify(editions));
|
||||
|
||||
- name: Prepare release scripts
|
||||
if: ${{ contains(github.ref, 'release-x') }}
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Get build dependencies
|
||||
if: ${{ contains(github.ref, 'release-x') }}
|
||||
uses: actions/github-script@v9
|
||||
id: dependencies
|
||||
with:
|
||||
@@ -112,26 +93,22 @@ jobs:
|
||||
const version = getVersionFromReleaseBranch('${{ github.ref }}');
|
||||
const requirements = getBuildRequirements(version);
|
||||
|
||||
return {
|
||||
java_version: requirements.java,
|
||||
node_version: requirements.node,
|
||||
};
|
||||
|
||||
core.setOutput('java_version', requirements.java);
|
||||
core.setOutput('node_version', requirements.node);
|
||||
|
||||
build:
|
||||
needs:
|
||||
[check-existing-artifacts, get-build-requirements]
|
||||
if: always() && !cancelled() && needs.check-existing-artifacts.outputs.should_build == 'true' && (needs.get-build-requirements.result == 'success' || needs.get-build-requirements.result == 'skipped')
|
||||
name: Build MB ${{ matrix.edition }}
|
||||
needs: [setup]
|
||||
if: ${{ needs.setup.outputs.build_editions != '[]' }}
|
||||
name: build (${{ matrix.edition }})
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
edition: ["ee", "oss"]
|
||||
edition: ${{ fromJson(needs.setup.outputs.build_editions) }}
|
||||
env:
|
||||
MB_EDITION: ${{ matrix.edition }}
|
||||
INTERACTIVE: false
|
||||
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }} # faster builds for dev
|
||||
SKIP_LICENSES: ${{ github.event_name == 'pull_request' && !inputs.containerize }} # faster builds for dev
|
||||
# Emit bundle stats on trunk/release builds (tracked over time) and on PRs
|
||||
# (the bundle-size check reads them for both the PR head and its base ref).
|
||||
EMIT_BUNDLE_STATS: ${{ (github.ref_name == 'master' || startsWith(github.ref_name, 'release-') || github.event_name == 'pull_request') && 'true' || 'false' }}
|
||||
@@ -143,11 +120,11 @@ jobs:
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
with:
|
||||
node-version: "${{ needs.get-build-requirements.outputs.node_version || '22' }}"
|
||||
node-version: "${{ needs.setup.outputs.node_version || '22' }}"
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
java-version: "${{ needs.get-build-requirements.outputs.java_version || 25 }}"
|
||||
java-version: "${{ needs.setup.outputs.java_version || 25 }}"
|
||||
- name: Build
|
||||
run: ./bin/build.sh
|
||||
- name: Upload bundle stats
|
||||
@@ -168,29 +145,23 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# Same key the artifact lookups use, so a build for an explicitly dispatched commit
|
||||
# uploads where `check-existing-artifacts` and `check-jar-health` go looking for it.
|
||||
# uploads where `setup` and `jar-healthcheck` go looking for it.
|
||||
name: metabase-${{ matrix.edition }}-${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
path: |
|
||||
./target/uberjar/metabase.jar
|
||||
./COMMIT-ID
|
||||
./SHA256.sum
|
||||
|
||||
check-jar-health:
|
||||
jar-healthcheck:
|
||||
needs: [setup, build]
|
||||
# Health-check whatever we are about to containerize, plus every build on master and release.
|
||||
if: ${{ !failure() && !cancelled() && (inputs.containerize || github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) }}
|
||||
name: jar-healthcheck (${{ matrix.edition }})
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
name: Is ${{ matrix.edition }} (java ${{ matrix.java-version }}) healthy?
|
||||
needs: [build, check-existing-artifacts]
|
||||
# We only care about the explicit JAR health check on push to `master` and the `release-` branches.
|
||||
# JARs created in random branches on PRs are 99% of the time treated as ephemeral, and used only in e2e tests.
|
||||
# E2E tests already have a step where they wait for the instance health check.
|
||||
if: |
|
||||
always() && !cancelled() &&
|
||||
(github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) &&
|
||||
(needs.build.result == 'success' || (needs.check-existing-artifacts.outputs.should_build == 'false' && needs.check-existing-artifacts.outputs.ee_exists == 'true' && needs.check-existing-artifacts.outputs.oss_exists == 'true'))
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
edition: ["ee", "oss"]
|
||||
java-version: [25]
|
||||
edition: [ee, oss]
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
@@ -198,7 +169,7 @@ jobs:
|
||||
uses: actions/setup-java@v5.2.0
|
||||
with:
|
||||
java-package: jre
|
||||
java-version: ${{ matrix.java-version }}
|
||||
java-version: ${{ needs.setup.outputs.java_version || 25 }}
|
||||
distribution: "temurin"
|
||||
- run: java -version
|
||||
- name: Retrieve uberjar artifact
|
||||
@@ -217,3 +188,19 @@ jobs:
|
||||
java --add-opens java.base/java.nio=ALL-UNNAMED -jar ./metabase.jar &
|
||||
- name: Wait for Metabase to start
|
||||
run: while ! curl 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
|
||||
|
||||
containerize:
|
||||
needs: [build, jar-healthcheck]
|
||||
if: ${{ inputs.containerize && !failure() && !cancelled() }}
|
||||
name: containerize (${{ matrix.edition }})
|
||||
strategy:
|
||||
matrix:
|
||||
edition: [ee, oss]
|
||||
uses: ./.github/workflows/containerize-jar.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
edition: ${{ matrix.edition }}
|
||||
commit: ${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
release: false
|
||||
build-args: |
|
||||
GIT_COMMIT_SHA=${{ inputs.commit || github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
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: 84 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 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: 81 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: 77 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 76 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: 75 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |