Compare commits

...

230 Commits

Author SHA1 Message Date
Fraser
64fb24e355 Split api/analytics.ts into search telemetry and the export mutation
The file bundled two unrelated concerns. Search tracking now lives in
api/analytics/search.ts next to its only caller, and the export
mutation in api/analytics/export.ts. The analytics/ directory satisfies
the no-analytics-import-outside-analytics-files rule that pinned the
old filename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 15:23:08 +12:00
Fraser
74b08a51c8 Update module-boundaries config (#79733) 2026-08-12 14:33:34 +12:00
Fraser
244700d893 Create shared value-formatting module (#77771) 2026-08-12 13:19:22 +12:00
Romeo Van Snick
2a3f535c39 Prefetch a code-split page when a link to it scrolls into view (#79688) 2026-08-12 10:39:11 +10:00
Denis Berezin
eebccaad10 UXW-4973 Correctly show cache durations (#79499)
* UXW-4973 Correctly show cache durations

* Add validation

* Review fixes

* Fix CI

* Review fixes

* Fix e2e test
2026-08-12 02:09:14 +03:00
Andrei Chernykh
3b936768f0 Show pinned items in the collection contents list (#79679)
* Show pinned items in the collection contents list

* Update e2e specs for pinned items appearing in the contents list

* Simplify the collection empty state offset to a plain vh value

* Simplify bookmark state, skip the pinned query in trash, and drop the dead toolbar prop

* Use a real pointer click for the pinned item menu so hover cannot be raced away
2026-08-11 16:00:16 -07:00
Kevin Kelleher
1d56ea8657 Use consistent icon for explorations (#79728)
use consistent icon for explorations
2026-08-11 21:58:28 +00:00
Kevin Kelleher
4981a05a19 Include library in Entity and Mini Pickers after downgrade to OSS (#79678)
include library in Entity and Mini pickers after downgrade to OSS
2026-08-11 17:38:30 -04:00
Mihael Konjević
bd8bab1d36 fix: run source-card field-type inference before cross-stage inference (BOT-1604) (#78799)
* fix: run source-card field-type inference before cross-stage inference (BOT-1604)

The repair pipeline mini-resolves earlier stages to type a later stage's
cross-stage field refs, but ran that pass before stamping base-type onto a
source-card stage's own bare-name field refs. The mini-resolve of a
source-card-sourced prefix then always failed schema validation and was
silently skipped, leaving cross-stage refs untyped and the query permanently
unrunnable for any multi-stage construct_notebook_query built on a saved
question/model.

* docs: fix stale pass numbering in repair pipeline docstring (BOT-1604)

Number the new source-card field-type pass 4.5 and keep the pre-existing
cross-stage pass as 5, instead of bumping cross-stage to 5.5 - this leaves
the other in-file references to "Pass 5" correct and matches the file's
existing fractional-pass-number convention.
2026-08-11 23:28:22 +02:00
Mihael Konjević
56117f5bb8 Surface metric explicit-join dimensions via agent feedback (BOT-1612) (#74989)
- metric-details surfaces the metric's FK-less join dimensions (which the
  consumer-framed dimension list silently drops), each carrying the exact
  portable join clause to paste into `joins:` and an alias-qualified field
  reference. Fail closed: an entry is emitted only when its join clause exports
  and targets a real table; join-key columns are excluded.
- the :no-fk-path repair error is now actionable, pointing the agent at the
  metric's /dimensions resource for the join to add.

Also fix a pre-existing option-key typo in resources.clj
(:with-queryable-dimensions / :with-field-values -> the ?-suffixed forms the
readers expect) so the plain metric resource correctly skips dimensions and
field-value fetches.
2026-08-11 23:14:06 +02:00
Chris Vandoodewaard
9b9b40a5f9 centralize linting workflows (#79673)
* init file

* fix linting

* delete files moved into lint.yml

* try parallel steps

* more parallel

* rework so that dependencies are ordered properly

* match naming

* remove whitespace

* update file-paths, add concurrency block
2026-08-11 11:15:25 -04:00
Denis Berezin
24e36a9f0e UXW-4712 Fix non-applicable empty series tooltip values for funnel bar charts (#79602) 2026-08-11 17:33:55 +03:00
github-automation-metabase
c2d55422d1 Auto-update documentation for master (#79680)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-08-11 10:21:23 -04:00
Timur Mashara
c0fe1712f6 docs - host the model button image in the repo (#79707)
* docs - host the model button image in the repo

The troubleshooting guide linked the model button screenshot from
https://www.metabase.com/learn/images/models/model-icon.png, which now
404s with no redirect. Learn images were moved out of the marketing repo
into a CDN bucket, and that key space has been renamed several times with
old keys hard-deleted rather than redirected.

This broke the nightly Update Docs job in metabase/docs.metabase.github.io
on Aug 8-10. It only goes green today because the link gate tolerates one
broken link and the other one got fixed upstream.

Commit the image here instead of chasing the CDN. It matches how every
other image in docs/ is referenced, it gets versioned along with the
release branch so older doc snapshots keep the image they shipped with,
and it is the only form our link checkers actually validate: lychee runs
--offline (external URLs skipped) and the docs site's htmlproofer runs
--disable-external.

The source file was a WebP served with a .png extension, so it has been
transcoded to a real PNG - pngcrush in optimize-images.yml would have
failed on the original bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Optimized 1 images, saved 1.8 KB (1.7% smaller)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-08-11 16:16:18 +02:00
Egor Grushin
24b402cf95 Fix icon and text vertical alignment in incremental transform alert (#79681)
fix(ui): align Alert icon with text when there is no title

Icon box was locked to the 24px title row, so title-less alerts (e.g.
incremental transform warning) hung the icon above a single line of text.
Without a title, icon and close button now center on the first message line.
2026-08-11 15:04:31 +01:00
Timofey Kachalov
ca89dc06f1 Dev Data app fixes (#79604) 2026-08-11 17:50:44 +04:00
Chris Vandoodewaard
698956f32f milestones should only be updated/expected in metabase/metabase (#79539)
* onyl run set-milestone in metabase/metabase

* don't trigger cloud issues if this isn't metabase/metabase

* run ci?
2026-08-11 09:05:08 -04:00
Nemanja Glumac
4b10b24deb Run the block-master-source-branch-prs workflow conditionally (#79706) 2026-08-11 11:37:00 +00:00
Paolo Rascunà
0635ba585d Add prascuna to Metabot team and Slack map (#79704)
Co-authored-by: Paolo Rascuna <1936871+prascuna@users.noreply.github.com>
2026-08-11 12:30:14 +02:00
Andrei Simionescu
e9d98d62c3 BOT-1953: Suppress the Metabot sidebar on the conversation route (#79574)
* BOT-1953: Suppress the Metabot sidebar on the conversation route

The full-page Metabot surface disables the app-bar button and the Cmd+E
shortcut so the chat cannot open on top of itself. That check compared the
pathname against /question/ask exactly, but #77549 added a second route
rendering the same MetabotAsk, /metabot/conversation/:convoId, which
MetabotAsk itself redirects to after the first message. The sidebar came
back from there on.

useIsFullPageMetabot matches either route and drives both suppression
sites. useIsAskPage stays strict for MetabotAsk's redirect, which must not
re-fire once it is already on the conversation route. The route path now
has one spelling, Urls.CONVERSATION_BASE_PATH, so a rename cannot silently
turn the suppression off again.

The conversation matcher pins the route shape rather than a prefix, with an
optional trailing slash because react-router matches the route either way.

Cmd+E had no coverage because a note in the test harness claimed tinykeys
shortcuts do not fire under jsdom. They do, bound to Control rather than
Meta since jsdom reports an empty navigator.platform.

* BOT-1953: Match the Metabot route predicates with react-router

useIsAskPage compared the pathname to /question/ask by string equality,
so /question/ask/, which react-router serves as the same page, left the
app-bar button and Cmd/Ctrl+E active on a full-page Metabot surface.

Both predicates now go through matchPath, the matcher the router itself
uses: trailing slash and case tolerated, extra segments and an empty
conversation id rejected.

* BOT-1953: Keep the urls module to plain URL builders

The two route predicates move into the metabot hooks that use them, which
now call matchPath directly; urls/ goes back to exporting only functions
that build strings. Their url-level specs go with them, since the metabot
ui and app-bar button specs already pin the behavior.
2026-08-11 09:18:45 +01:00
Fraser
a6b5e09200 Count dynamic imports as module boundary edges (#79693) 2026-08-11 17:42:21 +12:00
Romeo Van Snick
fa3af4772e Give the reference section its own chunk (#79641) 2026-08-11 15:20:08 +10:00
Romeo Van Snick
8350e6cc4a Fetch the AI auditing pages when their route is matched (#79565) 2026-08-11 11:54:21 +10:00
Denis Berezin
2f3d32a2ba UXW-4983 disable download to pdf for empty dashboard tab (#79490)
* UXW-4983 disable download to pdf for empty dashboard tab

* Add tooltip

* Update Loki Snapshots

* Review fixes - hide button when dashboard is empty

* Update Loki Snapshots

* Fix e2e

* Review fix

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-08-11 00:45:44 +03:00
Kevin Kelleher
c9da277bd3 Fix dashboard click behavior for date columns without a unit (#79664)
add fallback unit for formatting unbinned date columns
2026-08-10 17:43:06 -04:00
github-automation-metabase
d225a5f8d0 Auto-update documentation for master (#79622)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-08-10 16:09:01 -04:00
Jeff Bruemmer
16d281da2a docs - custom viz embedding web components (#79403) 2026-08-10 19:57:07 +00:00
Chris Vandoodewaard
7592d39e42 combine all notification workflows into issue-notifactions.yml (#79659)
* combine all notification workflows into issue-notifactions.yml

* try proposed naming schema

* add newline

* remove unused on-new-feature-label job
2026-08-10 15:42:21 +00:00
Ryan Laurie
9e6e08bffb remove dead workflows and support code (#79624)
* remove dead workflows and support code

# Conflicts:
#	.github/workflows/build-for-release.yml
#	.github/workflows/main.yml
#	.github/workflows/presto-kerberos-integration-test.yml

* remove dead clojure files
2026-08-10 09:32:38 -06:00
Andrei Chernykh
9bdf46f3d7 Stop autopinning models and metrics on creation (#79548)
* Stop autopinning models and metrics on creation

* Fix e2e assertions that expected new models to be pinned

* Remove negative-case pin tests per review
2026-08-10 08:26:16 -07:00
Chris Vandoodewaard
814b57b74a only publish Github release for major and gold releases (#79478)
* only publish Github release for major and gold releases

* pr review fixes
2026-08-10 10:16:58 -04:00
Denis Berezin
570a0e18c8 UXW-4929 Resolve unknown string dashboard ids (#79586) 2026-08-10 16:43:48 +03:00
Romeo Van Snick
bfa63e2a92 Replace the Bun globals in the yarn.lock alias script to fix the type check (#79650)
* Use node:fs in the yarn.lock alias script to fix the type check

* Wrap the alias script in a main function instead of top-level await
2026-08-10 14:14:54 +02:00
Chris Vandoodewaard
ec9e8c40f0 post warning about deprecated labels (#79618)
* post warning about deprecated labels

* cleanup double/triple backport logic, fail if labels set

* fix message, whitespace

* silent api response
2026-08-10 08:00:34 -04:00
Romeo Van Snick
518ca39d56 CI: rebuild node_modules from the bun store on install (#79644)
Rebuild node_modules from bun store to avoid stale CI cache
2026-08-10 13:47:08 +02:00
Kevin Kelleher
0fc5582ae2 Prefetch queries from Exploration toolbar navigation (#79615)
* move prefetch up into ExplorationPage

* fix e2e
2026-08-10 07:37:22 -04:00
Romeo Van Snick
b750f8d8fa Split documents into its own chunk (#79349)
* Add lazyModalRoute for modals under a code-split page

* Split documents into its own chunk

* Prefetch the documents chunk from links into it

* Prefetch the comments sidesheet with the document page
2026-08-10 21:02:14 +10:00
Romeo Van Snick
fc7d75b01f Fix Snyk scan by renaming bun's aliased yarn.lock keys (#79646)
* Fix Snyk scan by renaming bun's aliased yarn.lock keys

* Trigger the Snyk workflow on changes to the alias fix script
2026-08-10 21:02:02 +10:00
Uladzimir Havenchyk
af266d1426 fix: pass isSensible to custom viz (#79325)
* fix: pass isSensible to custom viz

* fixup

* address review

* fixup

* address review

* address review

* fixup

* fixup

* address custom viz re-render

* fixup
2026-08-10 12:12:01 +02:00
Andrei Simionescu
660b7ac2db BOT-1944: Fix two Metabot tool failures (#79498)
* Fix two Metabot tool failures surfaced by the reliability data

get_transform_details passed the transform's query map straight into the XML
formatter. clojure.data.xml reads a map in second position as an attribute map
and then throws on MBQL 5's namespaced :lib/type key, so every query-source
transform failed and only Python ones (no :query key) got through. That is 72
errors in 75 calls over 90 days, across 15+ instances. The query now renders as
SQL text, matching how the line below already handles :target.

create_sql_query has two implementations sharing one tool name, and neither used
the shared agent-error contract. The :sql profile's code-edit variant had no
error handling at all, so the :agent-error? exceptions validate-database-access
raises escaped uncaught and were tracked as tool failures. That variant errored
on 26% of calls against 0.04% for the :internal one. Both now delegate to
handle-agent-error, as the other tools do: the intended validation message goes
back to the model, and anything unexpected is rethrown so real defects stay
visible in the telemetry rather than being reported as successes.

Neither tool had a test; both now do.

* Address review: transform rendering and 403 classification

Transform source queries now render verbatim (data.xml escaped <, >, &,
which breaks old_string matching against the real query), python bodies
render, and the source type attribute is no longer a keyword literal.
Notebook-built queries fall back to EDN with the attached metadata
provider stripped, shared with transform->xml, whose string guard always
dropped the normalized (map) query.

validate-database-access tags the plain 403 from read-check so both
create_sql_query variants classify permission failures as agent errors,
and the pre-classification ERROR logs are gone; run-tool already logs
both paths at the right levels.

* Tolerate string queries in transform-query->text

Legacy transforms stored raw SQL strings under :query, and orphaned sources
skip normalization, so the query reaching this helper is not always a map.
dissoc on a string threw ClassCastException; the search formatter's old
string guard had tolerated exactly this shape. Strings now pass through
verbatim, and the helper's namespace gets its first tests.

* Use a doc link for escape-xml in the escape-xml-content docstring

* Read orphaned sources' string keys in extract-sql-content

Orphaned transform sources skip normalization and keep their JSON string
keys, so the keyword probes missed them and the model got EDN of a
string-keyed map instead of the SQL. Multi-stage queries now fall through
to the EDN branch too: stage 0 alone is partial SQL and hid the rest.

Also tags the read-check 404 alongside the 403, closing the race where
the database vanishes between the exists? check and the read.
2026-08-10 10:26:23 +01:00
Fraser
cc136d9725 Move the query pickers into querying (#79561) 2026-08-10 20:50:57 +12:00
Romeo Van Snick
aee69e552a Drop prettier/standalone from the expression formatter and load the printer on demand (#79639)
* Print expressions with prettier's doc printer instead of prettier/standalone

* Load the expression printer on demand
2026-08-10 13:13:28 +10:00
metabase-bot[bot]
4dc84c1615 [chore] Update E2E test timings with recent data (#79628)
Update E2E test timings with averaged data

Collected timing data from 7 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[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-08-09 09:13:08 +02:00
Nemanja Glumac
fb00ed06ef Remove the test-results S3 upload (#79552)
We no longer upload JUnit XML to S3, so the action and all 47 call sites
go. Test results stay visible through the JUnit reporter and CI
Conductor.

`id-token: write` drops from minimal-tests and transforms-python-stress-test,
whose only OIDC consumer was the upload's AWS credential handshake. It
stays in run-tests, which must keep granting it to drivers.yml's ECR login.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 00:41:53 +02:00
Anderson
7675914513 Add in-collection type filters with dynamic options (#79340)
* Add dynamic collection type filters

* Complete collection filter coverage

* Harden dynamic collection filters

* Simplify collection type filters

* Address review feedback on collection type filters

* Additional clean up
2026-08-07 18:57:13 -03:00
Anderson
28160265ec Implement in-collection text search (#79333)
* Add in-collection item search

* Add collection search end-to-end coverage

* Fix collection search navigation and coverage

* Add loading state and fix spacing for collection search

* Keep collection tables mounted during refetches

* A couple UI refinements

* Show collection search loading inline
2026-08-07 18:57:12 -03:00
Anderson
3ec36759e7 Backend: search and type-filter metadata for collection items (#79332)
* Add collection item search and filter metadata

* Add collection authority filters

* Cover collection search edge cases

* Keep snippet pinned-state behavior unchanged

* Never report snippets in available_models

* Remove collection authority filters

* Code cleanup

* Cover explorations in available models

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>
2026-08-07 18:57:12 -03:00
Denis Berezin
d8a19a330a UXW-4939 Fix crash on dashboard tab duplication (#79554)
Co-authored-by: azharizz <azharizzannada@gmail.com>
2026-08-08 00:34:11 +03:00
Chris Vandoodewaard
a9ca5965c4 Fix all actionlint findings in .github/workflows (#79606)
Brings `actionlint` (v1.7.12, the version CI pins) to zero findings, down
from 186.

Real problems, not just lint noise:

- `pr-env-destroy-manual.yml` used an unquoted YAML scalar containing
  ` #`, so YAML truncated the command at the comment marker and the step
  actually ran `echo "Destroying PR environment for PR` — an unterminated
  quote. Moved to a block scalar with the PR number in an env var.
- `yaml.yml`'s `yaml-linter` job read `needs.files-changed.outputs.yaml`
  without declaring `needs:`, so its `if` was always false and the linter
  never ran.
- `containerize-uberjar.yml` referenced `github.event.inputs.commit` in
  three places, but its `workflow_dispatch` declared no inputs.
- The three Slack notification workflows interpolated
  `github.event.issue.title` straight into a `github-script` body; it now
  goes through an env var.
- Several step-summary blocks wrote `echo '... ${{ ... }}'` in single
  quotes, which never expanded as intended.

The rest is mechanical: quoting `$GITHUB_OUTPUT`/`$GITHUB_ENV`/
`$GITHUB_STEP_SUMMARY` redirect targets, grouping consecutive appends
into `{ ...; } >> file`, replacing `ls | xargs basename` with a glob
loop, `A && B || C` with `if/then`, `[[ "$?" != 0 ]]` with `if ! cmd`,
and assorted quoting.

`containerize-jar.yml`'s `release` input is now a real `boolean` rather
than a string of `"true"`/`"false"`, which also removes the
`${{ 'true' }}` workaround at its call sites.

Adds `.github/actionlint.yaml` to declare the `arm-ubuntu-24-2core`
self-hosted runner label.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:53:02 -04:00
Nathan Voxland
7b58c182a3 Better management of database metadata (#79181) 2026-08-07 13:40:45 -05:00
John Swanson
a2bc99107e UXW-4967: two tier research catalog (#79109)
* Split the research catalog tool to keep its payload bounded

get_research_candidates returned every research-eligible metric with every
candidate dimension inlined — on instances with many metrics the JSON blob
overwhelmed the following LLM request's context.

Two-tier replacement:

- list_research_metrics (new): slim index — id, name, description
  (truncated to 150 chars), in_library — no dimensions. Optional q filters
  by substring on metric and dimension names.
- get_research_candidates now requires metric_ids (max 20 per call) and/or
  q, and returns a deduped shape: same-group dimensions resolve to the same
  underlying Field, so descriptive fields (name, types, interestingness)
  are stated once per dimension group, with each metric's dimension id and
  per-metric display name (renames are common) in
  dimension_id_and_name_by_metric. A q match beyond the cap is ranked
  library-first-then-interestingness, truncated, and stamped
  {truncated, shown, matched}. Argument-less calls return an instructive
  error instead of the catalog.

add_research_groups is unchanged — the ids the model echoes back are the
same per-metric dimension ids as before. explorations.selmer updated to
describe the new contract (factual sync only).

* Cap the list_research_metrics index

Load-testing with 3,000 metrics (~62k dimensions) showed the unfiltered
index was still a ~394 KB / ~100k-token blob — slim rows don't save you
when there are thousands of them. Above 500 matches the index is now
ranked library-first-then-interestingness, truncated, and stamped
{truncated, shown, matched} — the same contract as the
get_research_candidates q cap — so the model knows to narrow with q.
Under the cap, rows keep their natural library-first-then-name order.

At the 3,000-metric load the unfiltered index drops to ~66 KB; q-filtered
responses are unaffected.

* Code review cleanup

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>
2026-08-07 13:39:04 -05:00
Andrei Chernykh
b346394afd Convert LegacyChoropleth to TypeScript (#79497)
* Convert LegacyChoropleth to TypeScript

* Address review: FIXME on memoization, discriminated union for getMapProjection
2026-08-07 11:29:58 -07:00
Andrei Chernykh
62351c70d3 Convert remaining spec-only unit spec files to TypeScript (#79545)
* Convert remaining spec-only unit spec files to TypeScript

* Update frontend/src/metabase-lib/v1/queries/NativeQuery.unit.spec.ts

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
2026-08-07 10:23:37 -07:00
Alexander Polyankin
97e8d67036 Declare two request params the handlers read but never declared (#79594)
* Declare two request params the handlers read but never declared

Request decoding drops keys a schema does not declare, so a handler that
reads an undeclared key silently gets nil.

- POST /api/card/pivot/:card-id/query destructured `parameters` and
  passed it to `process-query-for-card`, but declared only
  `ignore_cache` (and `dashboard_id`) — parameterised pivot queries
  silently ignored their parameters. Declared exactly as the non-pivot
  sibling in the same namespace does.
- POST /api/notify/db/:id destructured `synchronous?`, the documented
  flag that makes the sync run synchronously, without declaring it, so
  every call ran asynchronously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add regression tests for both undeclared params

Each test fails on master and passes with the schema fix:

- pivot: reuses the native card whose `{{category}}` template tag is
  `:required`, so the query only runs if the request body's value reaches
  the query processor. Without the declaration the request 500s.
- notify: blocks the redefined `sync-database!` on a promise and asserts
  the response is still outstanding, which can only hold when the handler
  derefs the sync future. Without the declaration `{:success true}` comes
  back immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:18:08 -04:00
Eric Normand
62f8cd51ee Avoid row-constructor IN in multi-catalog Databricks sync (GHY-4263) (#79533)
* Avoid row-constructor IN in multi-catalog Databricks sync (GHY-4263)

On a multi-catalog Databricks connection, describe-fields and describe-fks
filtered schemas with `(table_catalog, table_schema) IN ((?, ?))`. Databricks'
planner is orders of magnitude slower on that form against
`system.information_schema.columns` than on AND-ed equalities: on a Unity
Catalog workspace with ~220k columns the same query drops from 3m27s to 2.0s
when the predicate is rewritten. sync-fields issues one such query per schema,
so a ~50-schema sync took hours and never ran to completion.

Emit `catalog = ? AND schema = ?` per pair, OR-ed together for multi-pair
calls, instead of the row constructor.

* Update comment
2026-08-07 17:07:50 +00:00
metamben
f1613d8e2e Honor report-timezone in SQL Server for time ranges (#79271)
Closes QUE2-770
2026-08-07 16:05:17 +00:00
Tyler Plude
f7aa6d0f40 [BOT-1929] Moonshot provider support for Metabot (#79515)
* groundwork cleanup & gap fixing in provider adapters

* moonshot / kimi BE support

* FE support

* fix provider name & default model to kimi k3

* clean comments
2026-08-07 08:57:02 -07:00
Nemanja Glumac
11091c1db2 Adjust the quarantine consumer for top level suites (#79547)
* Honor suite-level quarantine rules in the gate

/api/quarantine serves a top-level `suites` array of glob rules alongside
`tests`. A rule matching the job's reporting suite quarantines the whole
suite, so the gate passes without comparing failures test by test.

Globs match as an unanchored, case-insensitive substring and support `*`,
`?` and `{a,b}` — a subset of the minimatch semantics ci-conductor
validates against, compiled inline because this package carries no
runtime dependencies and CI runs it straight from the checkout.

Both response keys are read as optional, so the script and the server
need no lockstep deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop the driver "info" status

Suite-level quarantine rules say the same thing from CI Conductor, for
every suite rather than drivers alone: run it, but don't let it gate.

mage now reads only "skip" from ci-test-config.json, and no longer emits
a per-driver status; the workflows drop the plumbing that carried it,
along with the job summary and PR comment that reported an info driver's
real outcome. The quarantine gate's own verdict and CI Conductor's UI
cover that visibility.

"skip" stays: not running a cloud driver at all saves real spend and has
no equivalent in the quarantine endpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Remove dead checks from drivers

* Tweak comment

* Drop the "info" status from notification

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:55:54 +02:00
github-automation-metabase
55e63c2b6f Auto-update documentation for master (#79557)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-08-07 11:16:25 -04:00
Nemanja Glumac
eee8e4e4e9 Key the uberjar artifact on the dispatched commit (#79449)
The upload named the artifact after the event's own commit, while every
lookup around it — the concurrency group, check-existing-artifacts, the
bundle-stats upload and check-jar-health — keys on the dispatch input
first. A dispatch with an explicit commit therefore built that commit and
uploaded it under the dispatch branch's tip.

Inline the composite action into its only caller so the name comes from
the same expression as the lookups instead of a second copy of the
fallback chain.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:03:25 +02:00
Alexander Polyankin
ab962a316a Schemas for API docs (#79521) 2026-08-07 10:51:01 -04:00
John Swanson
12f261bdf1 ignore missing sandbox/perms features when syncing (#78735)
* ignore missing sandbox/perms features when syncing

for consistency with other code, we should not worry about whether the
premium feature in question is actually enabled. If sandboxing is
enabled and configured and we're runnning EE code we should respect it
when a new table/group/db is synced.

* dedupe implied rows when setting data permissions

build-database-permission and build-table-permissions merge the results
of their recursive implication calls with `merge-with concat`, and
several implication paths can produce the same row:

- setting view-data to :blocked implies `transforms :no` both directly
  and via the implied `create-queries :no`, so the DB-level path
  INSERTed the transforms row twice
- coalescing table perms to a DB-level row (all tables agreeing on a
  create-queries value) implies a DB-level `view-data :unrestricted`
  row, and the recursive table-level view-data call coalesces to that
  same row, so the table-level path INSERTed the view-data row twice

Nothing deduped the merged :to-insert list, so every such call silently
persisted duplicate data_permissions rows. Duplicate DB-level rows are
the arming condition for the UXW-4927 sandbox data leak: the
going-granular path for newly-synced tables only deleted one row per
(group, perm-type), and a surviving duplicate out-votes the new table's
:blocked row at enforcement time.

* make the going-granular DELETE+INSERT atomic

The bulk path for newly-synced tables issued its DELETE and INSERT
outside any transaction, so a failed insert — e.g. the unique-constraint
violation added in the next commit — could leave the DB-level rows
deleted with the table-level expansion half-written. Wrap the pair in a
transaction. Nested transactions are savepoint-based (see
metabase.app-db.connection), so this is safe under callers that already
run inside a transaction, like the Table after-insert hook.

* dedupe data_permissions and add a unique constraint

data_permissions had no unique constraint on
(group_id, perm_type, db_id, table_id), so nothing prevented duplicate
rows for the same scope, and duplicate DB-level rows armed the UXW-4927
sandbox data leak. Three v58 changesets (v58 is the oldest release this
will be backported to):

1. Delete existing duplicates, keeping the most restrictive perm_value
   per scope, lowest id as tie-break.
2. Add a generated column unique_perms_helper = COALESCE(table_id, -1).
   All three app DBs treat NULLs as distinct in unique indexes, H2 has
   no partial indexes, and MariaDB has no functional indexes, so a
   NULL-safe generated column is the only portable mechanism — same
   approach as metabase_table.unique_table_helper (v55.2025-05-19).
   Unlike that precedent the base column carries an ON DELETE CASCADE
   foreign key, which MySQL rejects for STORED generated columns
   (errno 1215), so MySQL/MariaDB use a VIRTUAL column; Postgres has no
   virtual generated columns and no such restriction, so it keeps
   STORED. Dedupe, constraint enforcement, and FK cascade verified on
   Postgres 17, MySQL 8.0, and MariaDB 10.6.
3. Add a unique constraint on
   (group_id, perm_type, db_id, unique_perms_helper).

Inserts stay fail-loud: after the previous commits, a constraint
violation can only mean a genuine bug, and it should abort the write
(the going-granular DELETE+INSERT now runs in a transaction) rather
than be silently absorbed.

The generated column is excluded from the model (after-select) and from
load-from-h2/dump-to-h2 copies, mirroring unique_table_helper. Tests
that inserted raw duplicate rows on top of the hook-created defaults now
set permissions through the API instead.
2026-08-07 09:23:47 -05:00
Kevin Kelleher
0f14b54daa Improve Explorations sidebar empty states (#78264)
* sidebar empty states

* fix e2e test

* fix tests after rebase on master

* keep empty failed/canceled threads in the sidebar so they can be restarted

* review comment
2026-08-07 09:45:45 -04:00
Timofey Kachalov
95c0c90c6a Support source: card for query hooks and passing aggregations/breakouts/filters (#79456) 2026-08-07 17:34:49 +04:00
Kevin Kelleher
637e1b4344 Explore further from brush filter (#78144)
* explore further from brush filter

* review comment

* fix tests, self review

* Code cleanup

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>
2026-08-07 08:58:13 -04:00
Kevin Kelleher
692050c2db Explorations prompt updates (#78228)
* prompt updates

* fix flaky test
2026-08-07 08:15:49 -04:00
Romeo Van Snick
258b1708da Split the query builder into its own chunk (#79342)
* Prefetch a code-split page when a link to it is hovered

* Show a fallback while the router hydrates a lazy route

* Split the query builder into its own chunk

* Move the query builder's action types off the actions barrel

* Split the question selectors into their own module

* Read the question selectors from the leaf module in the app shell

* Import the query builder's view components directly in table editing

* Keep a pending lazy navigation from being replaced

* Prefetch the query builder chunk from links into it
2026-08-07 18:36:21 +10:00
Timur Mashara
661e899223 how to use metabase (#79024)
* how to use metabase - initial update

* update dark mode styles

* update checklist items
2026-08-07 08:28:57 +00:00
Timofey Kachalov
b918b9e5f4 Improved Data App dev plugin to handle buildId to prevent diagnostic endpoint poisoning of data frop previous HMR builds (#79447) 2026-08-07 04:22:45 +00:00
metamben
aba318ec2e Preserve overrides of model based on sandboxed table (#79334)
* Preserve overrides of model based on sandboxed table

Closes QUE2-778
2026-08-07 01:12:05 +03:00
metamben
7171418bdb Use min_duration_ms in cache-strategy (#79326)
This matches configured properties like `refresh_automatically` but doesn't match the ones calculated in the BE, like
`avg-execution-ms`.
2026-08-07 01:11:47 +03:00
metamben
628a3220ff Fix parameter selection cutoff when refreshing the cache (#79272)
Closes QUE2-762
2026-08-07 01:11:29 +03:00
metamben
fe731bdb2e Preserve cell information in pivots (#79266)
* Preserve cell information in pivots

Closes QUE2-777
2026-08-07 01:11:15 +03:00
metamben
87fabef5e3 Prevent an analytics configuration error to shadow the timeout (#77098)
Closes QUE2-734
2026-08-07 01:10:58 +03:00
metamben
911c0ef816 Look up long display-name from previous stages (#77230)
* Look up long display-name from previous stages

Closes QUE2-729

* Narrow scope
2026-08-07 01:10:41 +03:00
Nemanja Glumac
0ec5c4ea19 Simplify failed backend artifact name (#79441)
* Drop the redundant job id from the granular-rerun artifact name

For most drivers `github.job` and `junit-name` are the same string, so the name
came out as `failed-tests-be-tests-snowflake-ee-be-tests-snowflake-ee-Driver
Tests-attempt-1`.

`junit-name` plus `artifact-suffix` already discriminate every call site in the
artifact namespace that run-tests.yml shares between app-db.yml, backend.yml,
drivers.yml and semantic-search.yml — including the `be-tests-postgres` job that
exists in both app-db.yml and drivers.yml, where the two derive the same
`be-tests-postgres-ee` junit-name but differ on suffix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop the rerun artifact restore from erroring when there is nothing to restore

An attempt that fails with nothing attributable to a var uploads no failed-tests
artifact, so the next attempt's restore legitimately finds none and falls back to
the full suite. That is the designed path, but `download-artifact`'s `name` input
treats a miss as an error, printing a red annotation on a job whose real failure
is elsewhere.

`pattern` filters the artifact list the action already fetches and downloads
nothing on zero matches, so this is the same single API call as before and costs
no extra rate-limit budget. The pattern is an exact literal, and artifact names
are unique within a run, so it matches at most one.

`merge-multiple` is deliberately left unset: a single match already lands at
`target/failed-tests`, and setting it would merge a hypothetical second match into
that one file — letting a matrix leg rerun another leg's failures — where the
default nests them and falls back to the full suite instead.


Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Vandoodewaard <chris.vandoodewaard@metabase.com>
2026-08-06 21:30:33 +02:00
Kevin Kelleher
0e5501550b Show explore further toast only when a page has queries (#79530)
don't show explore further toast until queries are ready, matching sidebar
2026-08-06 13:42:38 -04:00
Andrei Chernykh
ecbacf9720 chore: Convert metabase-lib/v1 field-value utils to TypeScript (#79075)
* Convert metabase-lib/v1 field-value utils to TypeScript

* Address review: normalize getRemappings to [value, label] pairs, adopt honest RowValue-keyed mapping types with no casts, add remapping utils specs
2026-08-06 10:22:58 -07:00
Andrei Chernykh
4d6c2d2dbe Render pinned collection items in a single grid of compact static cards (#79493)
* Render pinned collection items in a single grid of compact static cards

* Fix pinned-card e2e waits that relied on removed previews

* Register explorations as pinnable and movable drag types

* Address review comments
2026-08-06 10:11:12 -07:00
github-automation-metabase
978332fcf7 Auto-update documentation for master (#79495)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-08-06 12:46:07 -04:00
Chris Truter
109f3ec4fb Remove module dependency cycles around utils (#75719) 2026-08-06 17:17:51 +02:00
Chris Truter
17ff890b7d Skip reanalyzing search models on every row hook (#78286)
* Memoize search model hooks

Cache model hooks by the registered spec methods using a single-entry FIFO. This avoids repeated per-row hook construction while bounding reload-time cache growth and keeping invalidation best-effort.

* Trim the model-hooks cache test

The finally-clause recompute was dead: restoring the method restores the cache key, so
the probe's entry can never be hit again. Split the caching assertion out of the
invalidation test.

* Assert the model-hooks cache is warm before probing invalidation

The first call registers spec methods as it resolves models, so it caches under a key
that is stale by the time it returns. Probing from that state proves nothing: the miss
comes from the stale key, not from the redefinition.

* Note that the model-hooks cache key is only an invalidation token

* Set *warn-on-reflection* in the search spec test namespace

Clj-Kondo flags any interop call in a namespace that leaves the var unset; the
.addMethod probes are the first interop here.
2026-08-06 17:16:59 +02:00
Maksym Yakubych
94b673140b [Metabot] Pretty print tool call results (#79477)
* [Metabot] Pretty print tool call results

Split a tool call's result into Response, Structured output and Other
fields sections instead of dumping the raw JSON-encoded result map into
a single JSON code editor. Text output renders with real newlines, and
structured_output (also the kebab-case spelling kept by rows migrated
from the v1 format) gets its own section.

* Simplify the tool call result parser
2026-08-06 14:46:05 +00:00
Fraser
ceccf16d4d Delete thin wrapper selectors over settings reads (#79144) 2026-08-06 11:14:10 +00:00
Egor Grushin
cb55deead4 Gdgt 2975 hide or handle create index in read only mode (#79452)
* Centralize remote-sync read-only handling in useTransformPermissions

Fixes Create index (and other transform mutations) being offered in
remote-sync read-only mode and failing with 403. The hook now returns
combined readOnly plus granular permissionsReadOnly/remoteSyncReadOnly,
and all direct getIsRemoteSyncReadOnly usages across transforms are
replaced with hook values or props. Run button stays gated by
permissions only (running is allowed in remote-sync read-only).

* Rename RunButtonSection readOnly prop to permissionsReadOnly

* Drop redundant remoteSyncReadOnly from incremental switch disabled state

* Fix plugin setup order in indexes page spec
2026-08-06 09:29:20 +00:00
Egor Grushin
eadf23cd60 Add tooltip explaining Managed/Unmanaged index source (#79455)
* Add tooltip explaining Managed/Unmanaged index source

* addressing the PR's comments

* Lint against unchained ttag c() calls

* Revert "Lint against unchained ttag c() calls"

This reverts commit 9c25250638.

* Remove unused Icon import

* Fix formatting in columns.tsx
2026-08-06 10:25:31 +01:00
Egor Grushin
41b44d34d0 Remove duplicated Indexes heading on transform indexes page (#79448) 2026-08-06 10:24:42 +01:00
appleby
92b6b2120c Enable Kimi K3 and Deepseek V4 Flash 0731 via OpenRouter (#79207)
* Enable Kimi K3 via OpenRouter

* Enable DeepSeek V4 Flash via OpenRouter

* Raise the conversation-title output budget from 128 to 512 tokens

Reasoning models spend the output budget on reasoning before emitting the forced
tool call, so at 128 tokens the structured call often returns no tool call at all
and no title is saved.
2026-08-05 18:11:50 -07:00
Egor Grushin
4efc94ee84 Lint against unchained ttag c() calls (#79502) 2026-08-06 00:09:49 +01:00
Denis Berezin
db0985e3ee Fix sankey click behavior value passed to dashboard filter (#79380) 2026-08-06 01:02:29 +03:00
Kevin Kelleher
789d334847 Explorations (#73244)
* Fixed failing CI

* Add query timestamp, refactor error marker

* no-mbql-introspection pass

* Order temporal-axis exploration variants by date, not aggregation

When the row cap fires on a time series, aggregation-desc ordering keeps
the highest-count buckets scattered across the full range: gaps in the
x-axis, old spikes beating recent data. Temporal axes now order by the
temporal breakout desc (aggregation desc as tiebreak) before the limit,
so the most recent contiguous window survives.

- time-facet: order by the temporal breakout (second breakout) desc
- default: branch on dim type — temporal dims by recency, categorical
  dims keep aggregation desc
- per-value-time-series: gains the same date-desc cap (had none, so a
  single fine-grained series could blow query-caching-max-kb)
- all three also filter the NULL date bucket: it can't render on a time
  axis, and its position under ORDER BY DESC is warehouse-dependent
  (Postgres NULLs first, MySQL last), making cap survival
  non-deterministic otherwise
- temporal-pattern-day/hour unchanged (bounded, no cap interaction)

* Added debug info to interestingness calculations

* even less mbql introspection

* oops, delete .scratch

* Sidebar UI fixes - error marker, loading shimmer

* Interestingness >= 0.7

* Adjust prompts list animation

* Fix e2e

* Note that the LLM query planner is a POC, not used in production

* Fix searching for explorations

* Use llm-metabot-provider for everything

* Log interestingless calculation as info for now

* Log interestingless calculation back as debug

* Fix FE lint

* fix ai summary showing dirty while generating

* Use llm-metabot-provider

* collection breadcrumbs

* add back null values for temporal variants

* order by for temporal pattern variants

* fix top-n-other

* doh, fix temporal pattern queries

* fix name in viz header, don't show be error in tooltip

* sort numeric bins in heat map header, fix tests

* UXW-4483 Add matching colors for category + breakout charts (#75912)

* Fix CI

* Fix units, fix loki

* UXW-4522 Update last query run timestamp in background

* Fix loki

* Fix e2e

* Improve Explorations viz types (#75958)

* UXW-4483 Add matching colors for category + breakout charts

* improve viz types first pass

* add error boundary

* add row chart support for dimension_value_colors

* Fix typecheck

* fix tests

* better handling of segments with row fallback, fix null color bug

---------

Co-authored-by: Denis Berezin <denis.berezin@metabase.com>

* UXW-4402: Make explorations respect sandboxing/impersonation/db routing (#75572)

* Make explorations respect sandboxing/impersonation/db routing

* some self-review fixes

* more cleanup

* test fixes

* doh

* Fixing CI failures

* capitalize 'By' consistently

* Fixed CI failure

* add loki snapshots

* Review fix

* Fixes after merge

* Rework Explorations UX (#76300)

* comment on exploration viz first pass

* wip

* wip

* wip

* UX changes first pass finished

* code cleanup, fix, add tests

* address review comments

* Code cleanup

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>

* Refine explorations with metabot (#75326)

* teach metabot about new exploration ux

* refine explorations

* address self-cr

* pass over FE

* CR response

* fix test

* Add :model/Exploration to api + eid-translation model-imports

These modules reference :model/Exploration after merging the latest
explorations base; declare the imports to satisfy model boundaries.

---------

Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>

* Harden explorations runner:
- planner crash recovery
- Bound timeline scoring
- add metrics

* UX tweaks

* Additional logging of errors on ai summary, plus increased thinking budget

* Improved timeline interestingness calculation

* Handle sonnet 5 etc better

* Make explorations groups a real thing (#76643)

* Rename ExplorationThreadGroup model to ExplorationBlock

Explorations organize their queries in a hierarchy: an Exploration has
Threads (one per run), a Thread has Blocks, and a Block is one
metrics-by-dimensions grouping that renders as a single sidebar heading.
That persisted grouping entity was named ExplorationThreadGroup, and the
word "group" was overloaded across the backend and frontend. Rename the
entity to Block — the name the frontend already uses — so the larger
data-model changes that follow start from one clear name. No behavior
change.

Renames :model/ExplorationThreadGroup -> :model/ExplorationBlock and
table exploration_thread_group -> exploration_block.

- Append a renameTable changeset rather than editing the createTable in
  place: the original is already applied to running dev DBs, so an
  in-place edit would fail checksum validation and force a DB rebuild.
- Rename the model namespace/file; update all call sites, the require
  alias (:as block), and insert-blocks!.
- Update model registrations (resolution, serdes models, copy, entity-id
  test) and re-sort the entries.
- Refresh the model's docstrings to describe a "block".

The planner's :thread-groups ctx key and the read-side group-tree are
left untouched here; they belong to later read-side work.

* Persist exploration pages with stable ids reconciled across reruns

Within a Block, an Exploration's queries are organized into Pages: a Page
is the bundle of queries for one (metric, dimension, query_type). Until
now a Page existed only as a derived, content-addressed key computed at
read time, so anything that wanted to point at a Page had to reference a
string that changed whenever the grouping definition changed. This makes
a Page a real persisted row whose surrogate id is its identity, so
comments (and stars, coming next) can anchor to a Page and survive both a
rerun — where queries are deleted and regenerated — and a future
re-grouping.

Introduce ExplorationPage: the persisted (block, card, dimension,
query_type) page within a block.

- New exploration_page table + model, FK to exploration_block (cascade)
  and report_card. Registered in resolution, serdes (excluded), copy,
  and the entity-id test.
- Add exploration_query.page_id (NOT NULL, FK, cascade): the reconciler
  stamps it on every query, so it's required from the start. group_id
  stays for now as the block pointer the read-side and deep-link URLs
  use; dropping it waits for the read-side rework.
- Reconcile in insert-plan-rows!, the single insert point for both
  initial Start and rerun: find-or-create a page per distinct key
  (stamping page_id), then GC pages left with no queries. reset-thread-
  for-rerun! is unchanged — pages survive a query delete (cascade is
  page->query), so find-or-create recovers the same page ids on rerun.
- No DB unique index on the key: dimension_id is text (not portably
  indexable on MySQL) and planning is single-threaded, so uniqueness is
  enforced by find-or-create and matching is done in Clojure.
- Migration changesets appended (the table/group_id changesets are
  already applied to the running dev db).

Tests cover page-id stamping, rerun id-stability, orphan GC, and perms.

* Return nested blocks→pages from the read API; drop query.group_id

An Exploration Thread's structure is Blocks, each containing its Pages,
each Page bundling the ids of its queries. Until now the backend instead
returned a hand-rolled flat list of typed nodes (sidebar/singleton/page)
linked by parent_group_id and keyed by auto:<...> strings, which the
frontend then re-nested. Now that Blocks and Pages are real rows, return
the nested shape the tables already encode: GET /:id returns :blocks,
each with its :pages, built from ExplorationBlock + ExplorationPage rows
and queries grouped by page_id.

- New metabase.explorations.blocks (renamed from .groups): blocks-tree
  builds the nested shape; block headings + page names + score-sorting
  ported over; block-display-name made public for the LLM planner ctx.
- Deep links go to a page by id: page-url → /question/research/<exp>/
  page/<page-id>; the AI-summary and document-append builders pass the
  primary query's page_id.
- Drop exploration_query.group_id (and its FK/index); a query reaches its
  block through its page now — build-row-context (used by the runner to
  finalize each row's MBQL) resolves the block via the page, which is what
  the group_id read there used to do.
- API schemas: ExplorationQueryGroup → ExplorationBlockNode +
  ExplorationPageNode.
- Migration changesets appended (the table is live on the dev db).
- Rewrite the read-model + endpoint tests for the nested shape; every
  ExplorationQuery test fixture now hangs off a page (page_id NOT NULL).

The create payload key stays :groups for now (separate input contract,
touches the data-picker flow). FE consumption of :blocks lands next.

* Consume nested blocks→pages on the frontend

The frontend renders an Exploration Thread as sidebar headings — one per
Block — with a navigable item per Page (a Page being the bundle of
queries for one metric/dimension/variant). It used to receive a flat
typed-node `groups` list and re-nest it itself; this switches it to the
nested `blocks` array (block → pages) the read API now returns, and
renames the navigable unit from "group" to "page" throughout. Users see
the same sidebar and charts — this lines the frontend up with the
backend's shape.

- exploration.ts: add ExplorationBlockNode / ExplorationPageNode; thread
  `.groups` → `.blocks`; remove ExplorationQueryGroup / its display-type;
  the navigable id is now a page node id (kept as an opaque string).
- Sidebar utils build the tree directly from blocks→pages (one heading
  per block, one child per page); score-sorting + empty-block filtering
  preserved.
- The navigable unit is renamed group→page end to end: the route segment
  becomes /page/<id>, matching the backend's AI-summary deep links. The
  selection/url/comment-draft id stays a string via String(page.id), so
  the existing string-keyed plumbing is unchanged.
- Visualization toolbar/header/comment-draft props renamed group→page;
  the mark-as-interesting handler is left a stub for the stars feature.
- Specs, stories, and test-utils rewritten to the nested shape.

The create-payload request key stays `groups` (separate input contract).

* Retain comment-anchored pages across rerun GC; point comment URLs at /page/

A Page bundles the queries for one (metric, dimension, query_type), and a
comment can be anchored to a Page — comment.child_target_id holds the page
id. On a rerun an Exploration's queries are deleted and regenerated, and
any Page left with no queries is garbage-collected. A Page a comment
points at must survive that cleanup even when it ends up empty, or the
comment would resolve to nothing. This makes the rerun GC comment-aware
and moves comment deep-links onto the page route.

- gc-orphan-pages! now retains an orphan page when some "exploration"-
  targeted comment's child_target_id equals the page id (compared as
  strings). Empty + no comment is still GC'd as before. Page ids are
  globally-unique PKs, so matching on child_target_id alone is
  unambiguous.
- exploration-comment-url: /group/<child> → /page/<child>, matching the
  page route and blocks/page-url; stale "group ID" docstring fixed.
- explorations imports :model/Comment for the retention lookup (comments
  doesn't depend on explorations, so no dependency cycle).
- No backfill: the dev Postgres has zero exploration comments and zero
  auto:* child_target_id rows, so the optional migration is skipped.
- Test: comment on a page → drop that page's selection → rerun → the page
  is retained and the comment still resolves; a no-comment empty page in
  the same rerun is still GC'd.

* Generate short and long page names from metric, dimension, and variant

A Page — the bundle of queries for one (metric, dimension, query_type) —
is shown in the sidebar under its Block's heading, but is also referenced
on its own elsewhere (comment deep-links, AI-summary chart titles,
search). Page names are derived, not stored, so a Page needs both a short
label that omits whatever its Block heading already shows (e.g. under a
"Number of Orders" heading: "By Category over time") and a full,
self-describing label for context-free places ("Number of Orders by
Category over time"). This generates both from the Page's parts — the
metric name, the dimension name, and a short per-variant qualifier —
rather than by string-slicing a finished name.

- variants/variant-qualifier: query_type → a short, param-free localized
  qualifier ("over time", "(Day of week)", ...) or nil for the default
  breakdown. Lives next to query-name so all variant naming is in one
  place. Unit-tested.
- blocks: page-long-name and page-short-name generate from parts (metric
  + dimension + qualifier) using tru templates and the block's anchor;
  page-node emits :name and :long_name.
- API ::ExplorationPageNode and the FE ExplorationPageNode type gain
  :long_name (FE display wiring left for separate work).
- Same-dimension pages that differ only by variant (default vs
  top-n-other) are now distinctly named instead of colliding; a
  blocks-tree test that asserted the collision now asserts they differ.

* Add ability to star pages, sidebar tabs UX (#76755)

* add ability to star, sidebar tabs

* - renamed "user interestingness" to "starred"
- Let starred just be true|false, no null
- Removed DELETE starred API, just PUT now

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>

* Fix CI: test compile error, migration boolean type, e2e formatting

- documents/search_test.clj: qualify text->prose-mirror-ast with the
  documents.test-util/ alias in the exploration-thread exclusion test.
  The unresolved symbol broke backend test compilation, which cascaded
  to every app-db/driver test job and clj-kondo ("0 tests" everywhere).
- 20260428_explorations.yaml: use ${boolean.type} instead of bare
  boolean for exploration_page.starred (required post-v49 so MySQL
  uses BIT(1)).
- oxfmt formatting for two explorations frontend files (fe-lint).

* Update e2e specs for reworked comment URL + explorations sidebar

- comment helper: match the node comment link by path only. The
  `?new=true` marker was removed from comment URLs (base commit
  "respect query params in all urls"); the CommentsMenu unit test was
  updated but this e2e helper was missed. Verified the comment flow
  still works (opening a node's thread shows the new-comment editor).
- explorations spec: align with the reworked sidebar —
  * `by <dim>` leaf label is now `By <dim>`
  * Scratchpad doc test: only Scratchpad is created synchronously; the
    AI Summary doc is created asynchronously, so stop asserting it (and
    the removed "Findings" grouping) at creation time.
  * AI-summary completion test: the AI Summary doc row now hangs under
    the thread heading ("Initial investigation"), not a "Findings" group.
  * remove the two "Add to document" tests — that control was removed.

* Fix stylelint rule-empty-line-before in explorations CSS modules

Add blank line before nested &:hover rules in ActionToolbar.module.css
and ExplorationGroupVisualization.module.css. These surfaced in fe-lint's
lint-css step once the oxfmt formatting failure was resolved.

* - cleaned up migration file
- Improve group renaming in code
- preserve starts between re-runs like comments

* Fixing CI failures

---------

Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>

* Don't send empty messages to the LLM

* address review comments from 76755

* UXW-4765 Hide charts (#77488)

* UXW-4765 Hide charts

* Review fixes

* Fix the failing explorations unit tests and Loki stories  (#77564)

* Fix the failing explorations unit tests and Loki stories at the branch tip

* Restore the original all-hidden copy and drop thread headings from it

* Delete the outdated ExplorationGroupVisualization stories and their Loki references

* Rephrase the hide toast to put the chart name first

* UXW-4764 Remove interestingness badges on results (#77544)

* UXW-4764 Remove interestingness badges on results

* Drop e2e assertion for the removed sidebar interestingness markers

* Colocate the interesting and error markers with their only consumers

* Remove the interestingness marker from the timeline picker too

* fix types, lints, formatting

* Uxw 4779 update tab menu to match designs (#77543)

* UXW-4779 Update sidebar tab menu to match designs

* Show a pointer cursor across the whole SegmentedControl

* Give the icon tabs img semantics and hide the decorative icon names

* Let getExplorationSidebarTabsInfo own the tab icon config

* Revert the SegmentedControl root pointer cursor, Mantine already puts it on the labels

* Regenerate modules config after master merge

The merge left 'explorations' and master's new 'entity-retrieval' out of
sorted order, which metabase.core.modules-test asserts on.

* Float the action toolbar pinned to the bottom of the chart canvas (#77667)

* fix types

* revert timelines in documents

* Review fixes: stop requiring the QP's internal cache.impl from other modules

Explorations code that saves and loads query-result snapshots was requiring
metabase.query-processor.middleware.cache.impl directly — an internal
namespace of the query-processor module that other modules aren't supposed
to reach into. Since the module's :api set is generated from actual usage,
those requires had put cache.impl on the module's public surface — something
master never exposed. Modules talk to each other through a small public
facade (metabase.query-processor.core) instead.

The two helpers those callers need — do-with-serialization (write a QP
result as a gzipped blob) and with-reducible-deserialized-results (read one
back) — are now re-exported from the qp.core facade, and the explorations
callers go through it. One consumer can't: queries/cached_result.clj is
itself loaded while qp.core is loading, so requiring the facade from there
would be a circular-load error at boot. It gets a proper home instead — a
new small, documented public namespace,
metabase.query-processor.result-serialization, re-exporting the same two
helpers. With nothing outside the module requiring cache.impl anymore, the
regenerated :api drops it, matching master.

* Review fixes: explorations planner and AI-summary polish

- Nested cardEmbeds: the AI-summary postprocessor wrapped only top-level
  cardEmbed nodes in resizeNode containers, while the card materializer
  walks the whole document — so a cardEmbed the LLM emitted inside e.g. a
  blockquote got a real card but rendered collapsed to zero height. The
  wrapper now walks the tree too (skipping already-wrapped nodes).
- Awareness-tier sort validation: phase 2 validates that embedded
  categorical charts carry an explicit sort instruction, but the validation
  set was built only from top-tier breakouts — an awareness-tier categorical
  chart escaped validation entirely, and its prompt block (slim-block) never
  told the model a sort was required. Both tiers are validated now and
  slim-block emits the same per-chart sort directive as full-block.
- Contextual-scoring gate order: safe-score+describe resolved its scoring
  context (a Card select and a SQL compile) before checking whether the row
  is scorable at all (chart config present, creator known, non-blank thread
  prompt) — wasted DB reads and a query compile for every unscorable row.
  The cheap gates run first now. Its docstring also claimed the LLM gate
  throws and is swallowed; it actually returns nil from a pre-flight cond.
- Discovery-cache TTL: the top-K category discovery cache was a
  process-lifetime LRU, so a stale top-K result could be served forever on
  a long-lived instance; entries now also expire after 15 minutes.
- Chart-name i18n: filtered-subset chart names built their multi-value
  summary as (str (count values) " values") — a hardcoded, unpluralized
  English fragment inside an otherwise-translated name. It uses trun now.
- Docstring/comment corrections: effective-cardinality documented (and
  branched on) a :num-bins bucket that no caller can produce — branch and
  docs removed; run-one-plan-iteration! said it runs "the LLM planner" when
  the default is the mechanical one; a variants-test section comment claimed
  temporal variants add a not-null filter that no variant adds.

Also repoints this module's result-blob serialization calls at the qp.core
facade (the consumer half of the cache.impl cleanup in this stack's first
commit).

* Review fixes: explorations API permissions, documents, and search

- Creating an exploration accepted arbitrary card ids (in blocks) and
  timeline ids without checking the caller could read them, so you could
  attach someone else's private metric or timeline and see its name/events
  later. Both are read-checked at create time now, and timeline hydration
  filters out timelines the viewer can't read. A duplicated timeline id used
  to hit a unique constraint and 500 the request; ids are deduped.
- The restart endpoint re-selected the Exploration row after resetting the
  thread, but the reset never touches that row — the redundant select is
  dropped.
- GET /api/document listed documents that belong to exploration threads,
  which search, recents, and collection items all deliberately hide. It
  applies the same filter now — which as a side effect removes an N+1
  pattern where each listed doc could trigger several permission queries.
- The document validator rejected {:type "doc" :content []} — the exact
  empty document the backend itself creates for new threads. An empty root
  doc is valid now.
- Search: is_published was deliberately hidden from search responses on
  master (#66841) and this branch accidentally re-exposed it — hidden again
  in both search engines. Exploration descriptions are searchable in the
  legacy engine too, matching the new engine.
- Data picker: metrics and dimension groups now use the same
  interestingness filter (a metric could previously reference dimension ids
  that appeared in no group), search matches the displayed
  "<group> - <dimension>" text, and a collection-ids query that ran twice
  per request runs once.
- Consistency: the create event publishes after the transaction commits
  (observers could previously fire before the data was visible).

* Review fixes: metabot / LLM infrastructure

- Asking Claude for extended thinking while also forcing a tool call
  (tool_choice "required") produced a request Anthropic rejects with a 400.
  The guard that already handled this for structured output now covers the
  general tools path too.
- The e2e guard that stops tests from hitting a real LLM provider threw a
  raw MalformedURLException on unparseable URLs; it now fails closed with
  its normal friendly message — and it was moved out of a catch-all that
  would have rerouted it, so it fails loudly.
- The timeline tools handed the LLM broken XML (an attribute quote and a tag
  never closed) and interpolated user-written names/descriptions unescaped,
  letting content break the markup. Output is well-formed and escaped.
- Hardening/docs: the retryable-error check bounds its exception cause-chain
  walk at 10 (a cyclic getCause could loop forever); the
  contextual-interestingness response schema now really forbids extra
  properties (its docstring claimed it already did); docstrings about
  thinking model buckets, return shapes, and inputs corrected.
- Also repairs three test breaks that already existed at HEAD: an arity
  mismatch from the OpenRouter merge, a test asserting thinking blocks are
  dropped (they now stream as reasoning parts), and tests that didn't stub
  the LLM gate's newer usage/permission checks.

* Review fixes: lib-metric, metrics transforms, startup sync, interestingness

- group-by-source was re-exported for JVM Clojure but missing from the
  ClojureScript branch of lib-metric.core; added.
- Metric/measure display-info inherited a default :long-display-name even
  where the method overrode :display-name, so the two could disagree;
  they're set together now.
- A failure in the startup metric-dimension sync aborted server boot
  entirely; it's wrapped to log an error and continue.
- The interestingness "leftovers" backfill loaded every unscored field for
  the database into memory in one select; it now streams one field at a
  time, and the namespace docstring mentions this second recompute trigger.
- Added a unit test covering the dimensions :out transform (kebab-key
  renames + type-value keywordization).

* Review fixes: enterprise perms/sync/deps, cached card query, test quality

- Remote sync: an event payload that simply omitted a key used to satisfy a
  nil condition (e.g. "exploration_thread_id must be nil") — conditions now
  require the key to be present with the right value, checked with
  (find object k), which distinguishes a present-but-nil entry from an
  absent key. Vector values in removal conditions rendered as broken
  equality clauses; they render as IN.
- Dependency graph: smartLink nodes with nil/placeholder ids added nil to
  the dependency set (the sibling cardEmbed branch already guarded this);
  guarded, with regression tests for both branches.
- Test quality: the revisions exclusion test asserted the impl against its
  own private set, so it could never fail; it checks a hardcoded expected
  list now. Exploration bookmarks get real API coverage
  (create/list/delete/ordering/archived), and a mislabeled test string was
  fixed.

* Review fixes: v4 instance-analytics views with explorations usage sources

The AI-usage analytics views translate machine source names ("exploration",
"contextual_interestingness", …) into human-readable labels with a SQL CASE.
The explorations stack added its new sources to the v1 SQL files — but the
views actually installed on instances are v3, and nothing reinstalled them,
so users saw the raw machine names fall through the ELSE branch.

Adds v4 of ai_usage_log and metabot_conversations (v3 plus the new CASE
branches, with one consistent "Explorations" label — v1 said "Exploration"
for the source but "Explorations" for the profile), a migration that
reinstalls the views at v4, an updated pointer comment in usage.clj, and a
parity test that fails whenever a known source/profile is missing a CASE
branch in the latest views — exactly the drift that happened here.

* Review fixes: frontend (minimal) and e2e

Only the low-risk frontend fixes from the review pass, plus the e2e test
fixes; the other frontend findings are deliberately left out to keep this
branch's FE diff small.

- getDimensionIcon existed byte-for-byte in two files; the duplicate module
  is deleted and its one importer now uses the copy in
  common/utils/columns.ts.
- e2e: an assertion that searched for text inside another text node (and so
  always passed) is meaningful now; readSelectedText no longer breaks
  Cypress's retry behavior by asserting after a .then; and the seemingly
  pointless GET /api/metric/:id in seedMetrics got a comment explaining the
  side effect it exists for (it persists metric dimensions).

* Remove the LLM query planner

Explorations plans queries with the mechanical planner only now. Removed:
the LLM planner namespace, its prompt templates (plan.selmer /
repair.selmer) and prompt loader, the prompt-markdown renderers in the
planner-context namespace, the explorations-query-planner setting
(mechanical/llm/auto), and the :skip-no-llm exit for "planner requires an
LLM but none is configured".

pick-planner! now always returns the mechanical planner — kept as a seam
so tests can stub it and a future planner has an obvious dispatch point.
The planner-contract protocol and the shared context builder stay (they
are planner-agnostic), and the query_plan_transcript column stays (every
planner writes a transcript); its remark no longer describes an LLM
exchange.

* remove graph._dimension_value_colors (per bar colors)

* Add row count to explorations API (#77740)

Already available, we just need to persist it and thread it through the
API.

* Format fix

* Exploration UX... um explorations. (#76901)

* click thru

* better comment links

* chart avatars

* avatars

* allow subsubexplorations

* disable explore further for Other group in top n charts and dimension anchored blocks

* fix explore further for binned numeric columns

* comment highlighting, remove avatars

* comment label formatting

* map and table highlighting

* use built in viz click actions popover instead of custom

* fix comment badge in dark mode

* self review cleanup

* fix and simplify new thread toast

* fix be lints, add tests

* fix tests

* add fe, e2e tests

* - POST /:id/explore-further now verifies the clicked page
  belongs to this exploration before copying its block, closing a hole where
  any page in the instance could be copied into any writable exploration.
- key on the stable :explore-filters spec instead of a hash of
  the per-row-reconstructed :dataset_query (which minted fresh :lib/uuids and so
  never hit). build-row-context exposes :explore-filters on the row context for
  the cache to key on.

Also reuse metabase.util/capitalize-first-char in the block title and thread
name helpers instead of hand-rolled str/upper-case.

* Code cleanup

* Gate exploration derived metadata by viewer's permissions

* fix lints

* Code review improvements

* Den/uxw 4741 explorations new sidebar (#77674)

* Update tabs view, add filter button menu

* Fix tests

* Add sidebar items vertical lines + icons

* Other sidebar changes - viewed state, sticky hierarchy

* Show sub-explorations near it's parent item

* Fix issues

* Self review

* Fixes after merge

* Self review

* Review fixes

* Fix empty states

* Review fixes

---------

Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>

* Explorations self-review cleanup (#77717)

* Consolidate the explorations migration in place

The 063 explorations migration had grown by accretion: tables were created
and then altered by later changesets in the same unreleased file. Since the
branch convention is to edit unmerged changesets in place, the final shapes
are folded into the createTables:

- exploration_query gains params, nullable dataset_query/entity_id, and
  page_id inline (the page_id FK and index stay separate — exploration_page
  is created later in the file, so they can't run earlier).
- stored_result gains data_access_token inline; its backfill changeset was
  deleted outright — created and backfilled in the same release, it could
  only ever run against an empty table.
- exploration_query_result gains chart_stats / metric_description /
  chart_description inline, which also retires the randomly-named changeset
  id v63.gr324asd.
- idx_eqti_query_id deleted: the unique constraint on the same leading
  column already provides that index.

The completed_at and analysis_started_at backfills are deleted outright:
the explorations feature merges to master in one piece, so at upgrade time
no instance can have pre-existing exploration threads to backfill. (They
were also dangerous — they stamped columns the cancel endpoint and the
completion handler gate on being NULL, permanently locking out any thread
in flight during the migration.)

Cosmetics: backfilled default docs are named "Scratchpad" to match runtime
(the backfill said "Findings"); stale remarks corrected (chart_stats is EDN
not JSON, the real query_type variant list, what collection_id actually
defaults to).

Existing dev databases will need a migration reset.

* Set exploration thread started_at in the INSERT

The thread was inserted without started_at and then immediately updated to
set it, on the theory that the background worker must not see the thread
before its dependent document/block/timeline rows exist. The whole create
shares one transaction, so intra-transaction ordering is invisible to the
worker (it reads on another connection; the commit is atomic) — the flag
can simply be set in the INSERT, saving a write.

* Guard exploration restart against in-flight work

The restart endpoint could fire while the planner was still working on the
thread, deleting its rows mid-plan and wedging the thread in a state no
worker would ever pick up again. reset-thread-for-rerun! is now a CAS: it
applies only to a terminal thread (completed_at set) with no query still
mid-execution, and the endpoint returns 409 ("cancel it before restarting")
otherwise. A successful reset leaves the thread exactly in the shape the
planning worker's claim predicate picks up.

The endpoint also published :event/exploration-update, which provably did
nothing: restart changes no Exploration column, and the revision system
skips unchanged objects. The publish is removed; the AI-summary doc reset
still records a Document revision, which is what /mine's last-touched
ordering composes.

* Route mid-stream LLM errors through the provider-friendly rethrow

LLM responses stream lazily; a connection timeout between SSE chunks
escaped as a raw SocketTimeoutException because the provider error handling
only wrapped request setup — the reduction happens later, in the agent
loop. Every provider adapter's SSE reducible (claude, openai, openrouter,
azure, bedrock) is now wrapped so consumption-time failures get the same
translation as request-time errors.

* Grant the read-only timelines scope under metabot-nlq

The explorations metabot profile instructs the model to use the timeline
tools, but those tools were granted only under
:permission/metabot-other-tools while the profile is gated on NLQ — so for
NLQ-only users they were silently filtered out of scope while the prompt
kept referring to them. agent:timelines:* (whose only member is the
read-only list/details scope) is now granted under both permissions.

* Don't retry deterministically-failing fields in the interestingness leftovers pass

The leftovers pass selects on dimension_interestingness IS NULL, so a field
whose scoring deterministically throws was re-attempted on every sync
forever. A per-process attempted-marker now skips fields that already
failed in this process; a restart re-enables retry, so transient failures
still converge. Process-local by design — no schema change, and no sentinel
score leaking into product surfaces that read the column as a number.

* Fail closed when a sandboxed field's user attributes are missing

sandbox-token-for-table returned nil when the field was sandboxed but the
user's sandbox-attribute lookup came up empty — and the downstream
compatibility gate reads nil as "unrestricted". That's fail-open. It now
returns a user-scoped "indeterminate" sentinel that matches nobody else's
token (two indeterminate users don't match each other either): fail-closed.

Also adds direct and end-to-end tests for the impersonation and routing
token producers, which previously had none.

* Require the (card, stored_result) pairing on the cached card-query path

POST /api/card/:id/query with a stored_result_id read-checked the card but
then served whatever snapshot id the client sent — nothing verified the
snapshot actually belonged to that card. The pairing must now exist in
stored_result_use (404 when it doesn't, so an unpaired probe can't confirm
a snapshot exists). Both materialization paths already insert the pairing
rows, and the table ships in the same unreleased release, so no legacy data
lacks them.

The viewer-lens check also denied a nil dataset_query only by accident (a
schema exception swallowed by the catch — and in prod, where mu/defn logs
instead of throwing, it would have failed open); it denies explicitly now.
Plus: consistent t2/select-one forms in the endpoint, a stored_result_use
docstring update, and the generated module config picking up the new
StoredResultUse consumption.

* Apply the personal-collection default when it resolves after first render

The "default new explorations to my personal collection" logic captured
personalCollectionId once in a useState initializer — if the selector
resolved after the first render, the default never applied. It now
reconciles when the id arrives, without overriding an explicit user choice.

* Move the explorations migrations to v64

The v63 release branch has been cut, so the explorations feature will ship
in v64 and its migrations belong in the 064 directory. Both files move
(20260428_explorations.yaml and 20260519_exploration_bookmark.yaml) and
every changeset ID is reprefixed v63. -> v64.

No changeset content changes. Since we're already editing unreleased
changesets in place on this branch, existing dev databases needed a
migration reset anyway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v_metabot_conversations v4: restore include-deleted semantics lost in merge

Master's #77542 (conversation branches) and this branch both created
metabot_conversations/v4 in parallel: master's dropped the
deleted_at IS NULL filters so soft-deleted (rewound/regenerated) messages
are counted; ours added CASE branches for the new explorations usage
sources and profile. The 'Merged from master' resolution kept only our
copy, silently losing the deleted-message semantics — which is why
deleted-messages-included-test and last-message-at-test fail: they assert
master's behavior against our view.

This drops the three filter lines from each dialect's v4 file so v4 is the
union of both changes. Both installing changesets (our v63 and master's
v64) are runOnChange: true and checksum the referenced file, so every
instance reinstalls the corrected view on upgrade — no new version or
migration needed.

* Drop the redundant v_metabot_conversations v4 installer

Master's #77542 (064/20260713_metabot_analytics_views_include_deleted.yaml)
already installs metabot_conversations v4 via a runOnChange changeset against
the same SQL files, which now carry both sides' changes: the explorations
CASE branches and the include-deleted semantics. A second installer here was
redundant — and would have collided with master's changeset IDs
(v64.2026-07-13T00:00:00/01) had this file been mechanically renumbered to
v64 like the other explorations migrations.

The ai_usage_log v4 changeset stays: nothing on master installs that view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Move the ai_usage_log v4 migration to 064

Same reasoning as the explorations tables: v63 is cut, this ships in v64.
The changeset ID takes the next free slot for its date
(v64.2026-07-13T00:00:02 — master's include-deleted file owns 00 and 01)
rather than a mechanical v63->v64 rewrite that would have collided.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Show a crossed-eye indicator with a tooltip on hidden pages in the exploration sidebar (#77907)

* Explorations no AI Summary (#77878)

* Extract AI Summary + document-in-exploration out of explorations

Removes the LLM-generated "AI Summary" feature and the ability to display a
document within an exploration. The feature-restoring (inverse) commit is kept
on branch `explorations-ai-summary`; cherry-pick it to bring the functionality
back.

Backend:
- Delete the explorations.ai-summary.* pipeline, composite, document-perms,
  prompt templates, and their tests
- Drop exploration->Document creation, the /thread/:id/documents endpoints,
  the :documents hydrate, ai_summary_document_id, and the AI-summary reset
- Revert the documents-module coupling: content-visibility gate, the
  document.exploration_thread_id FK/column, the view-log skip, and the
  GET /document list filter; delete documents/validate.clj
- Drop the collection-items show_exploration_documents option and the
  exploration-doc exclusions in recents / search / serdes / remote-sync;
  drop the search exploration-thread-id attribute
- Remove the ai_summary_document_id and document.exploration_thread_id
  migration changesets (edited in place)

Frontend:
- Delete the ExplorationDocument component and its tests
- Remove the AI-summary sidebar node, completion toast, and the document
  API / urls / types; keep the shared comments sidesheet and page routing

* Extract static card-embed in documents out of explorations

Removes the static (cached-snapshot) card-embed capability from documents, so it
can land on its own branch (`static-embed`) independently of the AI-Summary
feature. The feature-restoring (inverse) commit lives on `static-embed`.

Keeps the base `stored_result` model, the exploration query-result cache, and the
cached-result permission helpers (`assert-can-view-cached-result!`) that the
exploration `/query/:id` streaming endpoint uses. Also keeps the general
`report_card.document_id` "card in a document" feature (it predates this work).

Removed:
- The cached-result serve path (`cached-dataset`, `apply-sort`,
  `deserialize-cached-result`, `allowed-chart-sorts`, `viewer-can-view-cached-result?`)
  and the `stored_result_id`/`sort` branch of `POST /api/card/:id/query`
- `stored_result_use.card_id` (column + model arm; keeps `exploration_id`)
- The documents serdes static-node filter (reverted to master's live-only rewrite)
  and the prose-mirror static docstrings
- Frontend: the tiptap `cardEmbed` static rendering (the `stored_result_id` + `sort`
  + `chart_href` trio) across the CardEmbed node/menu, document hooks, redux, and
  `card.ts`; deleted the CardEmbedNode static unit spec

* Fix pre-existing cljfmt/oxfmt formatting to unblock CI

These 4 files (a stray blank line each) are unchanged from the base explorations
branch — the format checks were failing on inherited, not extraction, changes.

* more documents cleanup

* Update Loki Snapshots

* change references from entity (which was page or document) to just page

---------

Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>

* Restore fix

* Fix Metabot exploration tools streaming empty results to the client (#77990)

Since 6912a50e85 ([Metabot] Chat internal format improvements, #77301)
only a tool result's :output string makes it onto the tool-output-available
SSE event; a bare map streams as "". The exploration plan tools returned
bare maps, so the FE — which applies plan edits by parsing the streamed
tool result — silently dropped every edit while the LLM (which still saw
the full result) reported success.

Wrap each tool's payload in {:output (json/encode ...)} and add a
regression test that runs real tool results through parts->aisdk-sse-xf
and asserts the payload survives onto the wire.

* Explorations timeline fixes (#77958)

* fix timelines

* disable timelines for row charts

* Explorations sort order (#77980)

* sort empty pages to bottom

* sort subexplorations by the initial exploration's interestingness

* Add e2e tests

* Explorations use persistent queue (#77901)

* explorations: run planning/queries/scoring on the persistent queue

Switch explorations from the hand-rolled polling runner to the mq persistent
queue: three queues (plan, query, timeline-score) with per-queue on-error
handlers and worker-count-bounded concurrency.

The generic mq subsystem changes this builds on live in a separate branch
(mq-queue-improvements) so they can land on master independently. Until that
lands and flows back into `explorations`, this branch depends on those mq
features (:on-error, :max-concurrent-batches) and won't run standalone.

* Remove explorations settings-test

* Code cleanup

* Code cleanup

* Use in-memory queue backend for e2e tests

The persistent Quartz queue backend stores its durable-job state in the app
DB's QRTZ_* tables. e2e restores app-db snapshots between tests (H.restore),
which drops those tables out from under the running scheduler while the
per-process ensured-jobs cache still believes the jobs exist. Post-restore
publishes then target a non-existent job and never deliver, so exploration
planning produces no queries and the detail-page sidebar stays empty.

e2e is a single node, so the in-memory backend (no app-db state, unaffected by
snapshot restore) is the correct fit. Set via the :e2e alias so it applies to
both local (runFromSource) and CI (runFromJar, via getJvmOptsFromDepsEdn).

* fix type

* Remove Timeline interestingness from explorations (#78013)

* Remove Timeline interestingness from explorations

Removes the LLM-driven per-(query, timeline) interestingness scoring
feature and all of its wiring

* Improve names (#77862)

* improve names

* review changes

* fix dimension disambiguation not working

* Fix explore-further on expression columns (#77900)

1. The 500 — normalize-target-ref (in metrics/transforms.clj) silently dropped the expression name. The :field ref schema has a legacy-form decoder that relocates the id into MBQL-5 position; the :expression schema has none, so a
  valid legacy ["expression" "Gear"] normalized to [:expression {…} nil]. That nil-named ref then failed Malli validation inside find-matching-column mid-request → 500.
  2. The drill couldn't work anyway — a top-n-other chart breaks out on a synthetic CASE expression that exists only in the variant query, never on the metric card. So even a valid expression ref could never resolve against the card.

* Code cleanup

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>

* Update Loki Snapshots

* Improved LLM error handling

* Fixed queueng of explore-further

* Fix e2e tests

* Add copy link hotkey support

* Fix e2e tests

* Fix CI

* Fix after merge

* Make highlighted state in line charts more obvious when hovering over comment badge (#77998)

* show tooltip when highlighting comment for cartesian charts

* set blurScope to blur across stacked chart grids, only show tooltip for single line series

* review feedback

* add types file that was supposed to be included with 77998

* self review

* Auto-switch the add metrics modal to the All tab when the library has nothing to show (#78070)

* Fix back button (#77995)

* Fix back button

Co-Authored-By: Andrei Chernykh <a.d.chernykh@gmail.com>

* Remove the stray /question/explore route, fix the entry spec's removed setMode prop, and clean up dead code

* Keep the research draft and conversation alive across the entry/plan step and browser back/forward via a shared provider on the research route

* Fold the plan-step URL assertion into the existing manual-flow e2e test instead of a separate one

---------

Co-authored-by: Ethan McCue <ethan@mccue.dev>

* Explorations: remove dormant extended-thinking support; unify LLM timeouts (#78173)

* Remove unused Anthropic extended-thinking support

Extended thinking was fully dormant: the Claude adapter supported it
(streaming translation of thinking blocks, config normalization, request-body
support) and call-llm-structured-with-trace forwarded a :thinking opt, but no
production caller ever passed :thinking, and the multi-turn agent path
(call-llm) never accepted it at all. Remove the adapter support and the
structured-path plumbing until a real consumer exists.

This also resolves several open review questions about the thinking code
(signature round-trip on multi-turn continuations, structured-output
guarantees under thinking, per-model capability guards) by removing the code
they applied to.

* Unify LLM HTTP timeout knobs across metabot.self and llm.anthropic

metabase.metabot.self.core/request hardcoded its connection (10s) and
socket (120s) timeouts, diverging from the metabase.llm.anthropic HTTP
path, which already reads the operator-tunable llm-connection-timeout-ms /
llm-request-timeout-ms settings. Operators had two uncoordinated knobs.

Delete the two private constants and read the settings at call time in
`request` (keeping the per-request `(merge req)` override). Raise the
setting defaults to the former constants' values (5000 -> 10000,
60000 -> 120000) so no path loses its safety margin, and move the
constants' streaming/inter-byte rationale into the defsetting docstrings.

Both LLM HTTP paths now share a single operator knob pair.

Resolves PR #1 review comment 2.

* UXW-4758 Ability to view all timeline events

* Show proper empty state message in sidebar for initial load

* Bump prefer-with-dynamic-fn-redefs ratchet to 28

The explorations MQ queue tests need three real with-redefs (not
mt/with-dynamic-fn-redefs): the handlers run on an MQ worker thread that
does not carry the calling thread's dynamic bindings, as documented
inline at test/metabase/explorations/queues_test.clj:102,153,265. Seed
the budget to match so metabase.core.kondo-ratchet-test passes.

* fix regression of #77667 caused by #76901

* Restore explorations CASE branches in v_metabot_conversations (v6)

master's #77549 rewrote v_metabot_conversations to v5 (summary->title
rename) without the explorations source/profile CASE branches that v4
carried, so the exploration/contextual_interestingness sources and the
explorations profile surfaced as raw machine names and failed
metabase-enterprise.metabot.usage-test/known-sources-and-profiles-have-view-case-branches-test
across every dialect.

v5's installer is not runOnChange and is already released on master, so
edit it in place would break the checksum. Instead add v6 (v5 + the three
branches) and a runOnChange installer that reinstalls the view after the
v5 rename, matching the v4 installer pattern.

* fix errors caused by merging master

* UXW-4758 Close timeline events sidebar when comments are opened

* Explorations queries api performance (#78175)

* revert error message display changes

* Avoid a full fingerprint version increment

* Fix unit tests error logs

* Removed the composed LRU + TTL cache that is not supported by the core.cache factories. The data should be naturally bounded enough that TTL should be good enough.

* Added scopes for explorations in metabot

* Explorations PUT safety (#78188)

Don't allow random assignment on PUT /api/exploration/:id

* Pick the right column as the metric for interestingness scoring (#78269)

Pick the right column as the metric

* Explorations unstranded threads (#78250)

Fixing ways threads can be stranded

* Hide SQL error messages from API response

* Explorations pass research plan (#78236)

Correctly pass research plan

* Revert in-place edits to historical instance-analytics view SQL

The explorations CASE branches were added directly to the v1/v4 view SQL
files, but those files back runOnChange changesets: editing them changes
the checksum, so on an already-migrated DB the old installers re-fire.
metabot_conversations v1 still selects c.summary, which the v64 rename
(summary -> title) has dropped, so booting the branch on a master-migrated
DB fails with 'column c.summary does not exist' (all six cross-branch
migration CI jobs).

The branches live on in the branch-new final versions (ai_usage_log v4,
metabot_conversations v6), so nothing is lost on fresh installs.

* Raise prefer-with-dynamic-fn-redefs ratchet to 29

The branch added four with-redefs ignores in explorations/queues_test.clj
(all justified: the redefined handlers run on MQ worker threads, which do
not see thread-local dynamic bindings, so mt/with-dynamic-fn-redefs cannot
work there) but the budget was only bumped to 28. Verified all 29 ignores
in the tree still sit on live with-redefs calls, so none can be removed
instead.

* Improving exploration error handling

* Fix with-current-user binding in exploration runner (#78163)

* Better error vs. empty handling in query cache

 Conflicts:
	src/metabase/explorations/query_plan/variants.clj

* self review

* Added FKs

* fix e2e

* Added visible-metric-cards-where-clause to queries.core

* Cleaned up removed-in-master agent:table scope

* Reuse Event card design from QueryBuilder

* Handle prompt injection

* Fix hotkeys hijack default actions

* Reuse the query instead of continually re-running it

* Fix extra rerenders on queries polling

* Review fixes

* Review fixes

* Fix chart skeleton for all screens

* review comments

* Fix Andrey's review comments

* Fix Andrey's review comments

* Fix Nemanja's review comments for e2e

* Fix Nemanja's review comments for e2e

* more review comments

* Ensure user has read permissions on cards when doing an explore-further

* Handle numeric PKs/FKs better in explorations (UXW-4757) (#77991)

Numeric PK/FK dimensions produced unusable one-bar-per-id charts and were
auto-selected as interesting dimensions. Two root causes:

1. The query planner treated any numeric dim as auto-binnable, but...
   they're not, and we emitted unbounded bare breakouts, and ruled out
   the top-n-other variant.

2. Dimension interestingness hard-zeros PKs but not FKs, and the
   cardinality scorer's auto-binnable branch gave numeric FKs 0.9 for a
   binned chart the QP can never produce, so they got auto-selected.

Fixes:

- default-bucket-for-dim returns nil for :Relation/* dims, so numeric keys
  band by raw distinct count: <=100 -> default, >20/unknown -> top-n-other.
- top-n-other stringifies its CASE THEN values so numeric dims compile to
  a single-typed CASE (mixed numeric/string CASE breaks strict warehouses).
- type-penalty hard-zeros numeric FKs (semantic FK + numeric effective/base
  type). Non-numeric FKs (e.g. a country-code FK) stay scoreable.
- The cardinality scorer excludes :Relation/* from the auto-binnable
  branch, matching lib's own binning rule.

* UXW-4475: self-heal missing metric dimensions in explorations read path (#78061)

Metrics whose dimensions were never successfully synced (dimensions NULL) -
e.g. metrics created on a model before it had result_metadata, or SQL-model
metrics predating the source-card sync fix - showed zero dimensions in the
explorations picker and Metabot research tools forever, because those paths
read persisted dimensions without ever syncing.

hydrated-metrics now runs sync-dimensions! for loaded metrics that have a
dataset_query but empty dimensions, then reloads the healed rows. Metrics
that compute nothing persist nothing and retry cheaply on a later request
(app-DB reads only, no warehouse queries, no write when nothing changed).

* Fix stored-snapshot permission gate ignoring card-sourced queries (#78455)

The data-access-lens capture derived its table set from
query->source-table-ids on the raw stored dataset_query, which only
collects numeric :source-table ids. For queries built on cards
(:source-table "card__N"), neither the creator's nor the viewer's token
picked up any tables, so both sandbox dimensions were empty and
data-access-compatible? returned true — letting a sandboxed viewer
stream an unsandboxed creator's snapshot.

Add query-perms/query->resolved-source-table-ids, which preprocesses the
query (as admin, via the existing preprocess-query helper) so source
cards — nested chains and card-sourced joins included — resolve to their
underlying table ids, and use it at both lens-capture sites (the
exploration runner's token write and the cached-result read gate). The
preprocess binds qp.i/*skip-middleware-because-app-db-access*, the same
flag the offline semantic checker uses, so the routing/impersonation
preprocess middlewares don't resolve the *viewer's* destination/role
(and throw on lapsed premium features) while we're only after the
query's table footprint. The helper throws when the card chain can't be
resolved (deleted card); non-admin viewers are then denied while
admins — never sandboxed, impersonated, or routed off the router db —
fall back to the same admin-only access as a nil token.

Also make stored_result.dataset_query NOT NULL via a follow-up
addNotNullConstraint changeset: the gate derives the viewer's lens from
it, so a query-less snapshot has no defensible verdict. A gate caller
passing a map without the key now gets a loud exception (500) instead of
a quiet fallback, so trimmed-row bugs surface in development.

Also fix explore-further-nested-thread-name-omits-metric-prefix-test to
reference a real exploration_page instead of a hardcoded id 999 that
violates the source_page_id FK on fresh databases.

* Fix build

* Fixed comment

* Better cached result testing

 Conflicts:
	src/metabase/queries/cached_result.clj

* Hash data access token info

* Added test from before

* Fix unit tests

* Fix Andrey's review comments

* Sloan review comments

* Explorations - fix metrics case (#78411)

* Add delay for Sidebar shimmer text, split ExplorationTreeNode

* Fixes for routing after merge

* Latest reviews fixes

* UXW-4908 new Comments UI

* add more analytics

* Code review fixes

* Fix scroll to view for focused comment

* Correct permissions "lenses" (#78697)

* Correct permissions "lenses"

Some early code had a huge, obvious bug, asserted in tests, for database routing.

For explorations, because queries are pre-computed, we need to calculate "lenses" representing the state
of database routing, sandboxing, and impersonation. These lenses are calculated when the exploration is
created, and then calculated when someone attempts to view the exploration. The idea is that if the lens
reveals that the viewer would have been able to view the underlying queries, then they should also be able
to view your exploration.

Until now, a viewer who was "unrestricted" on one of those dimensions was allowed to see a restricted
snapshot. I.e., a "non-sandboxed" user could see a sandboxed user's exploration, a "non-impersonated" user
could see an impersonated user's snapshot, and a "routed-to-the-router" user could see a "routed-to-a-destination"
user's snapshot.

The issue here... "unrestricted" does not necessarily mean "unrestricted." It's entirely possible that:

- User A is sandboxed with a sandbox that replaces Table T with card C, which actually queries Table FT.
  User B is permitted to see Table T (unrestricted) but is blocked on Table FT.

- User A is impersonated - to a *more* permissive role. User B is unimpersonated, but should not be able to see
  the results of a query made by User A.

- User A has database routing, configured to point to `MySecretTenantData`. User B is routed to `__METABASE_ROUTER__`,
  which holds a template schema and maybe some data to play around with. User B should not be able to see the results of
  A's exploration.

This simplifies the code here:

- superusers can see every exploration, regardless of lens

- everyone else sees an exploration's results *ONLY* when their data-access lens is *identical* to the creator's. Same sandbox
  (and relevant user attributes), same impersonation role, same routing destination. Unrestricted only matches unrestricted.

Also hardened the write side: capturing the creator's lens fingerprint used to be best-effort, silently saving a snapshot with
no fingerprint when it failed. Now the exploration query fails visibly instead, and the read side treats a
fingerprint-less snapshot as a bug (denied to non-admins, logged as an error) rather than something to adjudicate.

* Apply suggestions from code review

clean up test descriptions and other docstrings

Co-authored-by: Timothy S. Dean <timothy@timothydean.dev>

* Respond to CR

* fix impersonation test

---------

Co-authored-by: Timothy S. Dean <timothy@timothydean.dev>

* Fixed compile issue

* Address query-plan review comments on #73244

- context.clj: correct the column-fingerprint-for-target docstring (missing
  fingerprints make default/time-facet eligibility fail closed and route
  dims to top-n-other, not the reverse), fix the applicability return-shape
  doc, and extend the ns docstring to cover build-row-context.
- context.clj + variants.clj: remove the dead temporal_dimension_id override
  plumbing (nothing ever sets that param; the mechanical planner never emits
  per-value-time-series items).
- variants.clj: drop the unreferenced known-variants var.
- query_plan.clj + planner.clj: validate every planner result against
  result-schema in the orchestrator, treating a mismatch as a planner
  failure, and document that the schemas are enforced.
- planner.clj: fix the plan! docstring's claim that the ctx shape is
  documented on generate-query-plan!.

* Remove unused planner result schemas instead of enforcing them

Drops plan-item-schema and result-schema from the planner contract (review
follow-up on #73244): they were never applied to any value, and rather than
wiring validation into the orchestrator we document the plan!/result shapes
in prose on the protocol. Also rewords the context ns docstring's
plan/validate/materialize mention accordingly.

* Trim redundant clause from query-plan.context ns docstring

* Fix FE typecheck and unit tests

* Fix some tests

* Fix dimensions display in NewExplorationData

* Select all dimensions for a metric by default

* Fix metric-dimensions dependency test vs metric dimension auto-sync

The test (from master) created a metric card with hand-written
dimension_mappings and nil dimensions. On this branch a metric with nil
dimensions counts as uncurated, so the after-insert dimension auto-sync
seeds dimensions from the query and overwrites the provided mappings —
the mapped categories table never became a dependency.

Set the mappings via a plain update after insert instead: the
after-update hook only re-syncs when dataset_query changes, so they
stick, and the test still exercises what it means to — the dependency
backfill turning mapped-dimension tables into table dependencies.

* Fix metric dashcard perms test vs metric dimension auto-sync

The tests (from master) expect a metric dashcard query to return the
bare aggregation. On this branch the after-insert auto-sync seeds
metric dimensions, and qp.dashboard applies the default time dimension
as a breakout, turning the scalar count into a monthly series.

Suppress the auto-sync when creating the metric so the test stays
focused on what it exercises: permissions on the metric's source
model.

* Fix FE typecheck and unit tests

* Skip tests that will be addressed in the next PR

* Fix loki tests

* Add logs to identify e2e test failure reason

* Fix CI e2e failure

* Review improvements

* Redact raw driver errors from the exploration payload

`GET /api/exploration/query/:id` deliberately allowlists its 409 payload and
substitutes a generic message, because a driver error names tables, columns,
and row values from a query the reader may not have permission to run.

`GET /api/exploration/:id` hydrates the same `ExplorationQuery` rows onto its
response and returned `:error_message` verbatim, so the raw error reached
anyone who could read the exploration. Declaring the key in the response
schema does not help: a `defendpoint` response schema validates and encodes
but does not strip.

Give the substitution one home in `redact-query-error` and route both paths
through it — `hydrate-exploration` covers all five `::HydratedExploration`
endpoints at a single point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Track data_access_token with thread and query instead of relying on the (not always there) cached result

* Key the derived-data verdict on both query projections

* Remove eager dimension-sync for metrics

* Fix unit tests after merge

* Fix typecheck

* Code review fixes

* Revert "Select all dimensions for a metric by default"

This reverts commit d5e8883c95.

* Code review fixes

* Fixing tests

* Fix e2e test

* fix issues after merge

* fix error when saving exploration in Our Analytics

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Andrei Chernykh <andrei.chernykh@metabase.com>
Co-authored-by: Kyle Doherty <5248953+kdoh@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: Ethan McCue <ethan@mccue.dev>
Co-authored-by: Timothy S. Dean <timothy@timothydean.dev>
2026-08-05 21:34:13 +00:00
Anderson
76a9f014d5 Don't auto-assign a default dimension when creating a new metric / Allow for removing default (#79409)
* Don't auto-assign a default dimension to new metrics

* Update remaining metric tests for the no-default-dimension behavior

* Allow removing a metric's default dimension

* e2e tests cleanup

* unit tests cleanup

* Fix metric source replacement expectation

* address PR feedback
2026-08-05 17:00:09 -03:00
Phoomparin Mano
cac4d1e44e refactor: reusable typed schema codegen module (#78223)
* refactor: extract reusable typed schema module

* remove obsolete database-name query parameter

* refactor: split typed schemas rest endpoint module

* refactor: move public facade to core namespace

* refactor: use bare namespace for typed schema endpoint

* docs: describe typed schema options

* refactor: narrow typed schema query parameter

* docs: explain typed schema database names

* docs: clarify typed schema model scope

* re-export render-typescript from core namespace

* refactor: tighten typed schema options

* docs: add docstrings to typed schema query parameters

test: clarify typed schema parser coverage

* refactor: isolate typed schema model scope

* docs: clarify typed schema model scope

* test: simplify typed schema model scope coverage

* test: simplify rest api tests

* test: separate typed schema API and core coverage

* test: cover typed schema database selection

* test: assert generated typed schema tables

* test: normalize typed schema timestamps

* refactor: introduce TypeScript AST between schema and rendering

schema->ast is a pure policy stage producing a tagged-vector AST
(documented and malli-schema'd in typed-schemas.javascript); render-js
is an option-free printer over it. Replaces print-time policy callbacks,
path-based node-kind guessing, and the reference magic-map. Output
verified byte-identical against the previous renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: split typed schema fetching from pure assembly

fetch-items is now the pipeline's only impure stage; create-schema
assembles Items into the semantic schema value and takes an info map so
generated-at/instance-url can be pinned for deterministic output.
build-semantic-schema composes the two, unchanged for callers. Folds
the base-schema namespace into core, drops the duplicate
database-id-filter-clause, and rewrites core tests against literal
items. Output verified byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: reify typed schema data access as a protocol

SchemaSource names every read fetch-items performs; app-db-source is
the production implementation. Tests reify the protocol with literal
values instead of redefining selection functions across namespaces.
Output verified byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* test: add real-db coverage above the in-memory pyramid

source-test exercises app-db-source against with-temp rows — the one
seam where typed schemas meet the database. core-test gains a single
end-to-end test running every entity kind (question, metric, model with
action, table with fields) from with-temp rows through fetch, assembly,
AST and rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* test: run the pipeline e2e against the test-data dataset

mt/dataset provides a real synced database, so the e2e cards sit on
real tables and fields — the question and metric need no hand-written
result_metadata, and metric columns are computed from real metadata.
Replaces the hand-rolled temp database/table/field rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* test: assert the e2e schema with =? pattern matching

One structural pattern replaces the key-plucking assertions; a single
exactness check keeps the database-scoping guarantee that partial map
matching would silently lose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* test: sweep typed schema tests into =? pattern style

select-keys plucking becomes =? patterns (sets stay exact under =?, so
scope assertions keep their meaning); try/catch exception capture
becomes (is (thrown? ...)) + =? on ex-data; arg-asserting mocks become
stubs, with the argument pass-through now covered by the dataset e2e;
vestigial #' derefs on now-public fns are dropped; the five
prefer-with-dynamic-fn-redefs lint warnings are fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* docs: state the module's separation rules in namespace docstrings

Future changes should keep the boundaries: reads go through
SchemaSource, stages downstream of Items stay pure with environment
values entering via info, render policy stays schema->AST, TypeScript
syntax stays in the printer, and REST strings decode once at the
endpoint boundary. core-test documents where new tests belong in the
pyramid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: drop convenience arities orphaned by the source protocol

app-db-source always passes full arguments, so the single-argument
arities of select-schema-cards, select-tables, question-schemas,
metric-schemas and model-schemas had no production callers left. Each
fetch function now has one full-arity signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: define scopes and trim the library scope to its live keys

scope.clj now defines the vocabulary: references are portable ways to
name content (ids, names, entity ids); a scope is what they resolve to
on this instance — permission-filtered id sets with descendant
expansion, nil-vs-empty semantics, and the 404-vs-empty asymmetry
stated. LibraryScope gets a closed malli schema; its :collection-ids
and :library-collections keys were computed and asserted but read by
nothing, and are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: move scope compilation to the fetch layer as scope-filter-clause

scope.clj now only resolves references into scopes; the fetch layer's
schema.common owns the one function that compiles a scope into a where
clause. The docstring states the nil-vs-empty enforcement and warns
against (seq ...) guards at call sites, which would turn 'resolved to
nothing' into 'unscoped' and select everything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: batch collection reference resolution in typed schema scopes

Adds collection/descendants-flat-for (descendants of many collections
in one query, selecting :type) and rewrites scope resolution on top of
it: references partition into id and entity-id batches, resolve in at
most two selects, and 404s name every unresolved reference instead of
a bare 'Not found.'. Query count is now constant in the number of refs.
library-scope enforces its LibraryScope return schema via mu/defn.
Addressing, eligibility, and authorization are distinct layers in
resolve-collection-refs!, and their failures are deliberately
indistinguishable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: shrink the typed-schemas public surface to its contract

core now exports exactly three vars — SemanticSchemaOptions,
build-semantic-schema, render-typescript — mirroring the module's
data/print split; the pipeline implementation moves verbatim to the
internal pipeline namespace. New exports are compatibility promises,
and nothing needed the other five. The endpoint returns a ring map
directly instead of calling respond (async comes free), and
database-ids-for-ref uses select-one on the id path, documenting that
name references can genuinely match several databases (union semantics
pending a product decision).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* refactor: define the public API in core and name the build side

core now defines build-semantic-schema and render-typescript as visible
compositions of the internal stages instead of potemkin re-exports —
the public API is deliberate and decoupled from internal names. The
internal namespace becomes 'build': it interprets options into
SchemaSource reads and assembles the items into the schema value,
pairing with render/javascript as the module's two sides. The
compositions move out of build and render accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

* test: build e2e card queries with Lib instead of deprecated mbql-query

The e2e's cards now carry lib-built MBQL 5 queries
((lib/query mp (lib.metadata/table ...)) + lib/aggregate) rather than
hand-rolled legacy MBQL via the deprecated mt/mbql-query macro — which
also proves the pipeline handles MBQL 5 dataset_query cards end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB

---------

Co-authored-by: dan sutton <dan@dpsutton.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 02:39:53 +07:00
Egor Grushin
be5707555d Make indexed columns prominent in index list; fix name ellipsis (#79454)
* Make indexed columns prominent in index list; fix name ellipsis

* Revert test change
2026-08-05 20:09:25 +01:00
Kevin Kelleher
1974d2b34a Fix legends causing chart layout shift (#79458)
render legend and chart both only when non-zero width and height are provided
2026-08-05 15:01:23 -04:00
Sloan Sparger
0d1a8948b7 [Metabot] Increase max_tokens for anthropic models (#79030) 2026-08-05 17:25:15 +00:00
Andrei Chernykh
0b550259ba chore: Convert metabase-lib/v1 operator and expression utils to TypeScript (#78968)
* Convert metabase-lib/v1 operator and expression utils to TypeScript

* Address review: reuse api Field/Table types, extract format-options type, narrow operator name/type unions, add explicit return types

* Address review: throw via checkNotNull instead of optional chaining in operators spec
2026-08-05 10:05:26 -07:00
Andrei Chernykh
b408a5b32f Keep the fixed-width dashboard header from collapsing next to an open editing sidebar (#79431)
* Keep the fixed-width dashboard header from collapsing next to an open editing sidebar

* Update frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeaderView.module.css

Co-authored-by: Uladzimir Havenchyk <125459446+uladzimirdev@users.noreply.github.com>

---------

Co-authored-by: Uladzimir Havenchyk <125459446+uladzimirdev@users.noreply.github.com>
2026-08-05 10:05:00 -07:00
Nemanja Glumac
62383e86b7 Drop dead QueryExecution delete from embed download test (#79453)
Drop dead QueryExecution delete from embed download test

with-query-execution! redefs save-execution-metadata!* and captures the
query execution in a promise, so nothing is written to or read from the
QUERY_EXECUTION table. The delete! only served to reach the app DB before
anything had initialized it, failing with "this database is empty".

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 18:31:49 +02:00
Arik Aronov
919523d744 Security advisory version range display (#78649)
fix advisory version range display

Co-authored-by: Nicolò Maria Mezzopera <nicolo.mezzopera@metabase.com>
2026-08-05 16:32:03 +01:00
Sloan Sparger
19de9cb9b5 [Metabot] Remove unused profile temperature setting (#79457) 2026-08-05 09:53:13 -05:00
Nick Fitzpatrick
33b58efb9f Restructure Database form to handle engine change internally (#78932)
* Restructure Database form to handle engine change internally

* PR Feedback
2026-08-05 10:49:14 -03:00
Nick Fitzpatrick
8d598a796d Break out RemoteSyncSettingsForm component into smaller chunks (#79379)
* break out RemoteSyncSettingsForm component into smaller chunks

* PR Feedback

* liting
2026-08-05 09:18:51 -03:00
Andrei Simionescu
607459c994 Persist the download format on export, never on format switching (#79363)
* Persist the download format on export, never on format switching

The download popover remembers your last-used export format, but it was
persisting that preference on every click of the format segmented control,
spraying identical PUT requests at
/api/user-key-value/.../download_format_preference while the user just
looked around.

Switching formats now sends nothing. The preference is written as part of
handling an actual download, a fire-and-forget side task on top of the
export work, and only when it differs from the value the server already
has (the widget fetches it anyway to preselect the format, and the
mutation keeps the cached copy in sync, reverting it if the write fails).
Downloading with an unchanged format sends nothing at all. User-visible
behavior is unchanged: the popover still reopens on the format you last
exported with. The downloads e2e spec asserted the old
selection-persists behavior and is updated to the new semantics.

Considered and declined in review: guarding the comparison against a
still-loading preference (a download racing the initial GET can send one
redundant idempotent write; ordering a fire-and-forget path is not worth
it), and serializing overlapping preference writes (two different-format
exports within one HTTP round-trip could persist out of order; a mutation
queue for a preference value is overkill). Both races are rare, bounded,
and self-correcting on the next export.

* Trim the comments and the old-behavior test guards

Review feedback: the comment on persistExportFormat ran longer than the
function and said what the code already says, and both the e2e comment
and the no-request-on-switch assertions pinned behavior we removed
rather than behavior we built.

Also makes the optional chaining on formatPreference consistent within
persistExportFormat.
2026-08-05 11:48:41 +00:00
Phoomparin Mano
7b02363e37 useCurrentUser hook returns null in data apps (#79445)
load current user on data apps
2026-08-05 18:30:21 +07:00
Fraser
f6e0e1653e Reduce viz dependencies on static-viz (#79426) 2026-08-05 20:52:17 +12:00
Andrei Simionescu
062c0f7e85 Update browserslist data (caniuse-lite) (#79369)
Refresh the bundled browserslist data to clear the "caniuse-lite is
outdated" warning on dev builds; the data was several months stale.
Lockfile only: bumps caniuse-lite to 1.0.30001806 and
baseline-browser-mapping to 2.11.9, and drops stale nested duplicates so
they resolve to the hoisted versions. Hand-edited because
update-browserslist-db misbehaves with bun in this repo.
2026-08-05 09:40:36 +01:00
Chris Truter
42afd6b9c1 Manifest-based plugin framework (#78308)
* Support manifest-based runtime plugins

Summary

- Extract shared dependency pruning for runtime plugin uberjars.
- Initialize non-driver manifest plugins eagerly behind plugin API version 1.
- Preserve lazy loading for legacy driver manifests, while treating an empty driver list as a non-driver plugin.

Scope

This is capability-neutral infrastructure. It does not define an embedding API or ship an embedder.

Testing

- Plugin uberjar pruning tests.
- Plugin initialization, version-gate, empty-driver, and lazy-driver tests.

* Make runtime plugins lazy by default

* Document lazy plugin loader constraints

* Name plugin dependency checks after registration, not init

A satisfied `plugin:` dependency means the other manifest is registered, not that its
code is loaded. Rename the argument threaded through the dependency checks to say so, and
record where load ordering actually comes from for the two driver manifests that declare
one.

* Make a failed lazy-driver load retryable

Load the plugin before removing the placeholder `initialize!` method, so a load
that throws leaves the driver retryable instead of stranded on the no-op default.
Guard the removal so a driver that installs its own `initialize!` during load
keeps it.

Also clarify the `register-plugin!` contract (manifest plugins defer the classpath
add and code load; bare dependency JARs go on the classpath immediately), rename
`prune-provided-libs` to `prune-provided-libs!`, and mark its pure test parallel.

* Drop ^:parallel from prune-provided-libs-test

prune-provided-libs! logs via u/announce, so kondo's :metabase/validate-deftest
rejects the destructive fn inside a parallel test. Keep the ! name, drop the tag.

* Generalize bundled plugin loading to non-drivers; review nits

Rename bundled-driver-manifest-paths / load-bundled-driver-plugin-manifests! to
drop the driver-only framing -- the classpath glob already discovers any bundled
manifest -- and document that bundled plugins carry no add-to-classpath!, so their
code never comes from the writable plugins directory. Add a test asserting a
bundled plugin activates by running init steps only.

Also delay the plugin-uberjar core basis so requiring the namespace no longer pays
for a full dependency-tree resolution, and drop the unused init-plugin-with-info!
compatibility alias.

* Use with-dynamic-fn-redefs in the plugin init tests

Every binding here redefs a plain defn, so prefer the thread-safe dynamic redefs
(clears :metabase/prefer-with-dynamic-fn-redefs) and align each binding block's
values consistently.

* Register bundled manifests before plugins-directory manifests

Registration is first-wins, so a JAR dropped in the writable plugins directory
could shadow a bundled plugin's name and run its own code under that identity.
Load bare dependency JARs first (for class: deps), then bundled manifests, then
user manifests -- so bundled, root-owned code always claims the name.

* Reserve bundled plugin names so plugins-dir JARs cannot shadow them

A bundled plugin with an unmet dependency (Oracle/Vertica without their JDBC JAR)
never registers, leaving its name unclaimed -- a plugins-directory JAR could then
register under it and run as the bundled plugin. Reserve every bundled name up
front and refuse a colliding plugins-directory manifest.

* Reserve bundled names against the dependency batch too

Classification and registration read the writable JAR separately, so a JAR that
looks like a bare dependency but resolves to a bundled-name manifest could
register under it. Compute the reserved set once and pass it to both batches.

* Fail fast on concurrent plugin loading

* Align plugin framework forms

* Guard plugin registration and loading with one lock

Registration synchronized on the registered-plugins atom while loading claimed a second atom by compare-and-set, so
two different locks guarded two views of the same state.
Both paths now take one ReentrantLock: registration waits for it, and loading still fails fast when another thread
holds it.

Because that lock is reentrant, a plugin whose init step loads another plugin no longer trips the concurrency check
on its own thread -- nesting is not concurrency.

The registered and loaded name sets collapse into a single registry atom keyed by plugin name, which leaves
active-plugin-load as a diagnostic that names the loader in the rejection message.

* Reject plugin load cycles

A reentrant lock let a plugin load itself, directly or through a cycle like A -> B -> A.
`:loaded?` is only set once the init steps finish, so each pass through the cycle ran them again until the stack blew.

The lock now carries a stack of the loads in progress: nesting distinct plugins is still fine, re-entering one that is
already loading throws and names the cycle.
A rejected concurrent load no longer reports that plugin `nil` holds the lock when the holder is registering rather
than loading.

* Tidy the plugin cycle tests

Hoist the shared registration out of the first `testing` block, drop the closure that wrapped it, and share one uuid
between the two plugin names.

* Drop the temporary atom in update-unsatisfied-deps!

Everything here runs under plugin-lock, so a read and a reset does the job.
2026-08-05 09:57:14 +02:00
Romeo Van Snick
cf7bf4dd24 Converge LocationDescriptor onto react-router's To (#79338)
* Repoint navigation targets onto react-router's To

* Repoint location readers onto react-router's Path

* Pass the query builder's card state through navigate options

* Delete LocationDescriptor and toNavigateArgs
2026-08-05 14:16:23 +10:00
Anderson
fa991d5496 Allow drag-and-drop into sub-collections shown in the collection items table (#78839)
* Allow dropping items into sub-collections shown in the collection items table

* Trim comments in collection drop target

* Accept documents and metrics in collection drag-and-drop targets

* Improve collection drag-and-drop feedback

* code clean up

* Remove redundant React keys from drag source table row
2026-08-05 03:57:52 +00:00
Romeo Van Snick
55026d5ff5 Delete the v3 history handle from the test harness (#79297)
* Drive router specs through a v7-shaped test router handle

* Delete the v3 History type from the router facade
2026-08-05 11:57:21 +10:00
Romeo Van Snick
8d14aa8391 Retire redux navigation in favour of useNavigate (#79300)
* Retire redux navigation in favour of useNavigate

`push`/`replace`/`goBack` and the `CALL_HISTORY_METHOD` transport are gone.
Components navigate with `useNavigate`. Code that cannot call a hook (thunks,
the session middleware, a reducer, the permission and visualization config
objects) uses a module-level `navigate`, which is the navigator holder `AppShell`
already filled in, exported directly.

`metabase/router` exports its own `useNavigate`, which falls back to that
`navigate` when no router is mounted rather than throwing, because redux
navigation had no opinion on whether one was. The SDK mounts none, which is what
`useMaybeLocation` already exists for.

Four things this could not do mechanically:

* Relative targets resolved from the root under redux navigation, because
  `AppShell` sits on a pathless layout route. `useNavigate` resolves against the
  component's own match, so `SearchBar`, `SearchApp` and the command palette
  needed absolute paths.
* A `LocationDescriptorObject` is assignable to v7's `To`, so passing one to
  `navigate` type-checks and drops `state` silently. Six sites carried state,
  including the QB card-state contract.
* `mapDispatchToProps: { onChangeLocation: push }` cannot hold `navigate`, since
  `connect` dispatches what a bound creator returns. Five containers became
  function components, the dashboard context supplies it in the provider, and
  three `reference/` pages had it wired but never read it.
* A permission `actionCreator` returned an action to dispatch. It is now
  `onSelect`, returning void, and `updateDataPermission` treats the presence of a
  post action as handled rather than inferring it from a truthy return.

LOCATION_CHANGE and the location mirror stay: they are the router-to-redux
direction, feeding `isNavbarOpen`, `errorPage` and trace-id rotation.

* Hold navigations only when a router is on its way

A host that builds no router never registers a `navigate` for the holder to
flush against, so anything held is held for the life of the page. The SDK is
such a host and runs for a long time, so every `navigate` call from it retained
a closure and the `state` that closure captured.

`createAppRouter` and `createMemoryAppRouter` now say that a registration is
coming, because both put an `AppShell` above the tree. Without that, a
navigation with nowhere to go is dropped rather than queued, so a router-less
host retains nothing.

The queue stays a queue. Collapsing it to the latest navigation would change the
history stack: a push followed by a replace would apply the replace to the entry
the user arrived on, and lose the intermediate one.
2026-08-05 10:08:55 +10:00
Eric Normand
43915d7353 GHY-4251: Bound dependency backfill memory and stop its crash loop (#79392)
* GHY-4251: Bound dependency backfill memory and honour its off-switch

The BackfillDependencies job OOM-killed instances with large warehouses. Native-query dependency
calculation resolved parsed table names by fetching the Database's entire table catalog and scanning
it in Clojure, so cost scaled with warehouse size rather than query size. Reading an entity also
attaches a MetadataProvider to its query, and the job bound no provider cache, so each entity in a
batch got a private provider holding its own copy of that catalog -- all of it retained for the
lifetime of the batch. At batch size 500 against a ~20k-table warehouse that is ~10M retained table
rows, which no amount of heap fixes.

Three changes:

- Look up only the tables a query names. New :name-ci metadata spec matches names
  case-insensitively, since normalize-name lower-cases while sync stores whatever the warehouse
  reported (Snowflake reports upper-case). Unlike :id/:name it does not widen the result set to
  inactive or hidden objects, so it narrows an otherwise-unfiltered fetch without changing which
  tables are eligible. The SQL narrows and find-table-or-transform still decides, so matching
  semantics are unchanged. Transforms keep the existing scan -- their target name lives in a JSON
  column with nothing to query -- which is fine at their cardinality.

- Scope a MetadataProvider cache to the backfill batch, spanning the select as well as the
  processing loop, so entities share one provider per database instead of one each. This is the
  invariant deps.findings/upsert-analysis! already enforces by throwing.

- Honour a non-positive batch size in schedule-run! for both dependency jobs. It is the supported
  off-switch, but only task/init! checked it; the event-driven trigger did not, so entity changes
  kept waking the jobs every second to do nothing. schedule-run! is the one function all three
  scheduling paths share. The setting is env-var-only, so it cannot change while the process runs
  and the job stays unscheduled until restart.

* GHY-4251: Record the failure when a fatal error kills a backfill entity

backfill-entity-batch! caught Exception, so an Error -- an OutOfMemoryError in the reported case --
escaped without recording anything. The next run then selected the identical batch and died the same
way, indefinitely, with no progress and nothing in dependency_status to show for it.

Catch Throwable instead and record the failure before letting an Error propagate, so a poisonous
entity backs off and eventually goes terminal and the batch can drain. Errors are still rethrown
rather than swallowed: the rest of the batch is abandoned and the job fails, because the JVM is in a
state we cannot reason about. Exceptions keep their existing per-entity behaviour.

* GHY-4251: Only let pending retries keep the backfill job scheduled when licensed

The job reschedules itself when a full batch was selected or when entities are in retry backoff. The
retry check ran outside the :dependencies feature gate that guards the work itself, so an instance
without the feature kept rescheduling hourly forever: a retry marker is only cleared by processing
the entity -- on success via upsert-status!, or by going terminal past max-retries -- and both are
gated on the feature, so the job could never resolve the condition keeping it awake. It took the
"No more entities to backfill for, stopping" branch never.

Gate the check on the feature. Requires the feature to be enabled, and gives up an accidental warm
standby: with the feature off and retries pending the job previously stayed scheduled and would pick
up work the moment the feature flipped. Recovery does not depend on that -- setting the token
publishes :event/set-premium-embedding-token, and every content-change handler triggers the job -- so
the remaining exposure is an upstream-only change to a token's feature set, which starts the backfill
on the next content change rather than immediately.

* GHY-4251: Correct the off-switch rationale in schedule-run! docstrings

Both docstrings claimed the batch size setting is env-var-only. It is not: get-raw-value consults the
application database after the env var, and :setter :none only blocks writes through the settings API,
so a stored value would be honoured if one existed. The conclusion is unchanged -- there is no
supported way to change it at runtime, so a cold stop until restart is right -- but the stated reason
was wrong, and it is the reason a future reader would rely on.

* GHY-4251: Stop the entity check job rescheduling when it cannot do anything

DependencyEntityCheck is a periodic checker with no terminal state, so it always queues another run.
That is correct while licensed, but check-entities! is gated on the :dependencies feature while the
reschedule was not, so an instance without the feature woke every 30-90 minutes forever to do nothing.
Same defect as the backfill job's retry check, reached by a different route.

Extract the reschedule out of the Quartz job body -- it only needed the scheduler, not the whole
execution context, which also makes it testable -- and gate it on the feature. If the feature is
enabled later the job restarts from the content-change event handlers, or from task/init! on the next
restart.

* Update docstrings

* GHY-4251: Record a failure for entities that have no dependency_status row yet

record-failure! only updated an existing row, so it silently did nothing for an entity that had never
been processed. Those are exactly the entities instances-for-dependency-calculation picks up first --
it matches a null status row -- so on a first backfill every entity is in that state. A failure
recorded nothing, the entity was reselected unchanged on the next run, and the crash loop the previous
commit set out to break stayed intact for the population most likely to cause it.

Upsert instead, via app-db/update-or-insert! like the other writers in this namespace. An inserted row
takes the column defaults for stale (false) and dependency_analysis_version (0); 0 is below
current-dependency-analysis-version, which is what keeps the entity eligible for the retry once
next_retry_at elapses.

This also fixes the "failure count: null" log line in the backfill catch path, which read back a row
that did not exist.

Found in review by escherize.

* GHY-4251: Make the dependency jobs' reschedule decisions licence-failure tolerant

Two problems with gating the reschedule on has-feature?, both introduced by the previous two commits.

The entity check job had no event wiring at all, while the backfill job derives
:event/set-premium-embedding-token. Stopping its periodic chain when unlicensed therefore left no way
back except a content change or a restart, because the content-change handlers that would revive it are
themselves gated on the feature. Enabling the feature on an idle instance left the checker dead. Mirror
the backfill job's wiring so setting a token restarts it.

has-feature? also collapses "no licence" and "could not check the licence" into the same false:
*token-features* returns #{} on any Throwable, so a network timeout or tripped circuit breaker reads as
unlicensed. Where that decides whether to schedule it is terminal, since nothing re-fires when the check
later succeeds -- the token never changed. Both inputs to the backfill job's decision consult the
licence, so a single blip stopped that job too.

Resolve the licence for scheduling with canonically-has-feature?, which reports nil when it could not
tell, and stop only on a definitive false. No token at all still answers false, so the common unlicensed
case still goes cold; a token that cannot be validated keeps the job alive. Doing the licensed work
still uses has-feature? and still fails closed -- fail closed on work, fail open on scheduling.

Tests for both jobs now drive all three licence states. The previous two-state tests expressed
"unlicensed" with mt/with-premium-features, which only rebinds *token-features* and so cannot produce a
definitive false; they were passing for the wrong reason.

Found in review by escherize and metamben.

* GHY-4251: Select the analysis batch inside its metadata-provider cache

analyze-batch! selected its instances before entering with-metadata-provider-cache, so each entity in
the batch got a private MetadataProvider attached to its query -- the same shape as the backfill bug,
same default batch size, other job.

Unlike the backfill job this is not a live leak: upsert-analysis! ignores the embedded provider, builds
its own from the entity's database id inside the cache, and passes it explicitly to check-entity, so
those providers stay empty. It is a trap rather than a bug -- the moment an entity is analyzed through
its own query, a batch of 500 retains 500 providers' worth of fetched metadata. upsert-analysis! already
throws when no cache is bound, so the cache was always meant to cover this work; only the select sat
outside it.

Moving the select inside the cache is behaviour-neutral today. The new test pins the intent by comparing
provider identity, since providers compare equal when they wrap the same database id.

Found in review by escherize.

* GHY-4251: Back the dependency test fixture's Tables with the application database

The shared fixture composed its Cards and snippets over metabase.lib.test-metadata, so its Tables existed
only in memory. That is fine for metadata the provider legitimately supplies, but resolving a table name
parsed out of native SQL is a lookup against the appdb, and an in-memory base has no rows to find.

Swap the base to the appdb-backed provider. The H2 test-data database already has the tables these tests
reference, so nothing needs creating; expectations move from mock ids to the real ones. Cards and snippets
stay mocked, since nothing looks those up by name.

These pass both before and after the implementation change in the next commit, which is the point: they
verify behaviour rather than a particular lookup mechanism.

* GHY-4251: Resolve table names with a t2 query instead of a MetadataProvider spec

Narrowing the catalog fetch was implemented as a new :name-ci MetadataProvider spec key. That put the
contract on every implementation for the benefit of one caller, and they did not agree: the overriding
provider ignored the key and fell into its fetch-everything branch, the composed provider took a
fetch-then-refetch path, and the cached provider keyed it by whole-spec so batch sharing was poor.

Resolving a table name parsed out of native SQL is a lookup against the application database, not query
metadata, so do it directly: lib-be/tables-by-name, case-insensitive because normalize-name lower-cases
while some warehouses report upper-case. The SQL still only narrows -- find-table-or-transform continues
to decide -- so matching semantics are unchanged.

Two consequences worth naming. sql-tools now uses lib-be, where the provider previously hid the appdb from
it. And a native query naming a transform's not-yet-created output table now resolves to {:transform id}
via the transform branch rather than {:table <invented-id>} from an override; a dangling invented table id
was the worse answer, but it is a change.

Verified with the OOM repro: 0 catalog fetches, completes in a 1200m heap where master OOMs. Per-card appdb
query volume is unchanged from the spec-key version.

Suggested in review by Alex.

* GHY-4251: Reschedule after a failed run, and keep a disabled job warm

Two changes to when the dependency jobs schedule themselves.

A failure no longer ends the chain. Both jobs are one-shot self-rescheduling with no cron backstop, so an
Error escaping the batch skipped scheduling entirely and the job stayed dead until a content-change event
or a restart. That is survivable for an OutOfMemoryError, which usually takes the process with it and lets
task/init! recover on boot, but not for a StackOverflowError from pathological SQL, which leaves a live
process behind. The backfill job body is extracted to run-and-reschedule! -- it only ever needed the
scheduler, not the whole execution context, which also makes it testable -- and reschedules on the way out
of a failure before rethrowing.

A non-positive batch size now suppresses only the event-driven trigger rather than all scheduling. The
1-second trigger fires on every entity change and never consulted the batch size, which is what made a
disabled job wake roughly once a second; that is the part worth stopping. The job's own periodic schedule
is left alone, so a disabled job keeps ticking slowly and resumes if the setting ever becomes positive
instead of staying cold until a restart. task/init! schedules regardless and logs that nothing will be
processed.

Note the resulting asymmetry is deliberate: scheduling now fails open on an indeterminate licence but still
stops on a non-positive batch size, because the batch size is definitively knowable and a token's status is
not.

Raised in review by metamben; Error case from escherize.

* GHY-4251: Cover the case folding tables-by-name depends on

The narrowed table fetch is a prefilter, not the decision: it selects with SQL lower(name) against names
lowered by u/lower-case-en, then find-table-or-transform matches precisely using the driver's
normalize-name. That is only correct while the prefilter returns a superset of what the precise match would
accept, and one of the two assumptions behind that is testable -- the appdb's lower() has to agree with
Java's.

Add a test that stores tables under ASCII and accented-Latin names and looks each one up by its own,
lower-cased and upper-cased spellings. It needs no per-driver harness: the backend workflow runs it on H2
and app-db.yml runs it on Postgres and MariaDB, so a collation that folds differently fails it there.

Deliberately out of scope, and noted as such: locale-dependent folds like U+0130, where lower() legitimately
differs between engines. The other assumption -- that a driver's normalize-unquoted-name is a pure case
fold -- cannot be tested from here, so it is recorded as a comment at the call site.

* GHY-4251: Cover the other assumption behind the narrowed table fetch

tables-by-name is a prefilter and find-table-or-transform makes the decision, so the narrowing is only sound
while the prefilter returns a superset of what the precise match accepts. The previous commit tested one of
the two assumptions behind that and claimed the other could not be tested from here. It can: "a driver's
normalize-unquoted-name is a pure case fold" is just the assertion that lower-casing the input and the
normalized output gives the same string. Trimming, stripping or substituting characters all break it, and
any of them would make the prefilter drop rows that should have matched.

Each implementation is invoked directly rather than through driver dispatch, so no driver needs registering
or initialising. The test also asserts which implementations it found, because `methods` only sees those
loaded in the run -- without that it would quietly keep passing while covering less and less.

Comment at the call site updated: both assumptions now name their test rather than one being described as
untestable.

* GHY-4251: Avoid the deprecated mbql-query in the new findings test

The full kondo lint that CI runs reports cross-namespace deprecation warnings that per-file runs do not,
and CI fails on warnings. The new provider-sharing test used mt/mbql-query, deprecated since 0.61.0 for
returning legacy MBQL 4.

The test only needs two cards with distinct queries, so use native ones, matching the equivalent test in
backfill_test. That avoids the deprecation rather than adding a namespace-level exclusion for it.

* GHY-4251: Resolve table ids with t2 in sql-tools, then fetch metadata through the provider

Replaces the lib-be/tables-by-name added in 88b011c527. Resolving a name parsed out of native SQL is a
lookup, so sql-tools does that itself against the appdb and gets back ids only; the metadata for those ids
then comes through the MetadataProvider via lib.metadata/bulk-metadata, which is the ordinary :id spec.

Better than fetching the metadata directly on three counts. lib-be grows no public API. Overrides and
caching still apply to the metadata, because it goes through the provider. And the :id spec is cached
per-id, so entities in a batch naming overlapping tables share those entries -- the whole-spec keying of the
earlier version meant {orders} and {orders, products} were separate cache entries fetching orders twice.

The active and visibility filter moves into the id query: an :id lookup does not apply it, and the catalog
fetch this replaced was active-only, so it has to happen where the ids are chosen. The case-folding test
moves with the function, and gains coverage of that filter.

sql-tools no longer depends on lib-be. The module config records :model-imports #{:model/Table} instead,
which scopes the new dependency to the one model rather than a whole module.

Verified with the OOM repro: still 0 catalog fetches, completes in a 1200m heap where master OOMs.

Review feedback from Alex.

* GHY-4251: Drop an incidental reformat from lib_be/metadata/jvm.clj

Left over from the :name-ci spec key that was added and then removed. Restoring the line takes lib_be out
of the diff entirely, so this PR no longer touches any Querying Platform owned path.
2026-08-04 18:46:34 -05:00
Fraser
b633cdd03f Migrate small selector files to their domains (#79352) 2026-08-05 10:47:31 +12:00
Timofey Kachalov
821ed2e021 Adjust data apps skill to handle apps with height less than a viewport height (#79185) 2026-08-05 01:15:42 +04:00
Nemanja Glumac
e3ece7bdb4 Add granular reruns for backend tests (#78360)
* Add backend granular-rerun collector

Scan hawk's JUnit output and emit the failed test vars as a hawk `:only`
vector so a failed backend/driver run can be rerun targeting only those
tests instead of the whole suite. Reuses the existing backend JUnit
adapter. Writes nothing unless every failure is attributable to a var, so
an unattributable failure falls back to a full rerun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Wire granular reruns into the driver test action

On a re-run of a failed backend/driver job, restore the tests that failed
on the prior attempt and run only those via hawk `:only`, instead of the
whole suite. After a failed run, collect the failed vars and upload them
as an artifact keyed by junit-name; a re-run downloads it and targets those
tests. Falls back to the full suite whenever no confident failed-test set
exists (first attempt, missing/empty artifact, or unattributable failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Exempt stress workflows from granular reruns

Add a `granular-rerun` input (default true) to the driver test action,
gating the restore/collect/upload steps on it. Stress workflows set it to
false: they intentionally run a fixed test set `:times N` to hunt
flakiness, and narrowing a re-run to only the failed vars would drop that
repetition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Wire granular reruns into the non-driver backend tests

Extend the be-tests job with the same restore/collect/upload flow as the
driver action: on a re-run of a failed job, run only the tests that failed
on the prior attempt via hawk `:only`. The rerun drops the tag and
partition args, since the restored vars are already this partition's own
failures — re-partitioning the small set would drop half of them. Each
partition keeps a distinct artifact, so it reruns exactly its own failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Give app-db partition legs a per-job rerun artifact key

The granular-rerun failed-tests artifact was named solely from junit-name.
app-db's partition legs (Enterprise Tests, EE App DB Part 1/2) share one
junit-name, so they collided on a single artifact and a rerun could restore
the wrong leg's failed set. Add an optional rerun-artifact-key input to the
test-driver action and set a per-job-unique value for those legs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Collect hawk's var-less-errors JUnit file for granular reruns

hawk (PR #50) now writes fixture-init and namespace load/compile errors to a
fixed-name mb_hawk_var_less_errors.xml, with a testcase that has a name but no
classname. The backend collector only scanned *_test.xml, so it silently
ignored that file and the run's failing set stayed incomplete. Match the file
too; its classname-less testcase parses to a namespace-less failure, which
correctly forces a full rerun instead of a misleadingly narrow one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add a granular-rerun knob

* Scope the rerun artifact to the attempt that recorded it

The failed-tests artifact was named per job, so it outlived the attempt
that wrote it. An attempt that passed, or that failed with nothing
attributable to a test var, uploads no artifact, leaving the previous
one in place for the next attempt to restore -- narrowing a run that
should have gone wide.

Name it `...-attempt-N` on upload and read `attempt - 1` on restore, so
only what the immediately preceding attempt recorded is ever used and
anything else falls back to the full suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Compute the previous attempt number in the shell

Workflow expressions have no arithmetic operators, so
`${{ github.run_attempt - 1 }}` is not a valid expression -- actionlint
rejects it, and the runner would not evaluate it either.

Resolve it with `$((GITHUB_RUN_ATTEMPT - 1))` in a preceding step and
read it back from the env context.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear pre-existing actionlint findings in the stress workflows

These workflows are only linted when a PR touches them, so the findings
below sat unreported until this branch edited both files.

`transforms-python-stress-test.yml` omitted the required
`previous-step-outcome` input on all eight `upload-test-results` steps;
each `test-driver` step already carries `id: test-driver`, so wire its
`test-outcome` output through.

`drivers-stress-test.yml` tripped shellcheck: an unquoted `$JAVA_HOME`
(SC2086) and three consecutive appends to an unquoted `$GITHUB_OUTPUT`
(SC2086, SC2129), now a single quoted group redirect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Rename logs-artifact-suffix to artifact-suffix and reuse it for reruns

The failed-tests artifact needs a per-leg discriminator, which test-driver
cannot derive on its own: `matrix` and `strategy` are unavailable inside a
composite action, and `github.job` is the job id, identical across legs.
`logs-artifact-suffix` already carried exactly that value, so reuse it rather
than take a second input for the same concept.

Name both artifacts from `github.job` + `junit-name` + `artifact-suffix`.
Neither of the first two is unique on its own once run-tests.yml pulls app-db,
backend, drivers and semantic-search into one run and one artifact namespace:
`be-tests-postgres` is a job id in two of them and both derive
`be-tests-postgres-ee`.

Fill in the suffix wherever a matrix runs concurrent legs but did not set one,
or set one that did not separate them. Those legs already collided on the test
logs artifact; without this they would also restore each other's failed-test
set on a rerun and re-run the wrong tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Correct the var-less error description in the rerun collector

hawk writes var-less errors to mb_hawk_var_less_errors.xml without a
`classname`; the header claimed they leave no JUnit trace at all. What makes
them unusable for a narrow rerun is the missing namespace, not a missing file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let a colliding failed-tests upload fail instead of overwriting

The artifact name is scoped to the job and the attempt, so nothing legitimately
uploads it twice. With overwrite, a name collision would silently resolve to
last-writer-wins and a rerun would restore another job's failed set; without it
the upload 409s and says so. The step stays continue-on-error, so a collision
degrades to a full rerun rather than failing the job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear the restored failed-tests file when a rerun can't be narrowed

The collector signals "rerun everything" by leaving no file, but on a rerun that
path already holds the previous attempt's set, restored for the test step to
read. Declining to write left it in place, so the upload shipped last attempt's
list as this one's and the next attempt narrowed to it again — a namespace-load
failure could keep the full suite from ever running again.

Delete it on the no-confident-set path, and again in the CI step before the
collector runs, which also covers the collector dying before it decides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 23:09:44 +02:00
Fraser
96dd9ac122 Move Metabot editor extensions out of rich_text_editing (#79295)
* Move metabase:// protocol link helpers from metabot to urls

The metabase:// URL protocol helpers (parse/create functions, regex
constants, and entity model types) are a cross-cutting URL scheme used
by both metabot and rich_text_editing, so they belong in metabase/urls
rather than metabot/utils. Moved links.ts (and its spec) to
urls/metabase-protocol.ts, exported it from the urls index barrel, and
updated all importers.
2026-08-05 09:08:17 +12:00
Fraser
294ccb87c6 Move metabase:// protocol link helpers from metabot to urls (#79117) 2026-08-05 09:02:26 +12:00
technomancy
aa65891204 Edit partition migration to only drop identity if it exists. (#79415)
For unknown reasons, the DB on stats does not have the id column as an
identity, but this is causing migration to fail.
2026-08-04 14:01:41 -07:00
Fraser
fead86be0a Create settings module (#78981) 2026-08-05 08:59:19 +12:00
Egor Grushin
ece1b7cefb Metrics explorer editor race fix (fixes a bunch of flaky tests) (#79128)
* fix race condition

* fix race condition

* prettifying

* drill-through beforeEach: wait @dataset after each run, brush raced second-series re-render
2026-08-04 20:52:45 +01:00
github-automation-metabase
c461aa579e Auto-update documentation for master (#79402)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-08-04 19:33:57 +00:00
Phoomparin Mano
68bfbd63f4 Move the date picker CSS instructions to main skill file (#79378)
move the datepicker instructions up to skill.md
2026-08-05 02:03:08 +07:00
Oleksandr Yakushev
5482bdf418 [test] Cache repeated values in metabase.sync.sync-metadata-test (#79276) 2026-08-04 20:15:28 +03:00
Oleksandr Yakushev
3652ff05f6 [test] Shorten quartz test backoff delay (#79275) 2026-08-04 20:15:15 +03:00
Michael Eby
c29f00d480 docs: correct Cloud query timeout to 20 minutes (#79390) 2026-08-04 12:59:17 -04:00
Alexander Polyankin
4eddba236d Enter the metric-expansion admin context once, not once per metric (#79064) 2026-08-04 11:32:03 -04:00
Jeff Bruemmer
78abe86574 docs - analytics events generation (#79377)
* fix regex

* cleanup

* edits
2026-08-04 15:28:50 +00:00
Anderson
56eec114ae Increase horizontal spacing between inline chart setting label and toggle / Convert ChartSettingsWidget.styled to css module (#78922)
Add horizontal spacing between inline chart setting labels and their widgets (convert emotion file to css module)
2026-08-04 11:56:19 -03:00
Riley Thompson
1103c1d219 Move sql driver to mbql5 compilation, remove sql-mbql5 driver (#77529)
* mysql mbql5

* clickhouse mbql5

* starburst mbql5

* sqlite mbql5

* presto-jdbc mbql5

* druid-jdbc mbql5

* vertica mbql5

* athena mbql5

* hive-like mbql5

* fix databricks

* fix path

* sqlserver mbql5

* oracle mbql5

* fix sqlserver tests

* fix test failures

* fix sqlserver test errors

* clean up

* kondo, test fix

* remove sql-mbql5

* remove from more drivers

* update empty string is null driver

* simplify helpers

* remove unneeded defmulti

* fix field clause

* sqlserver mbql5

* oracle mbql5

* fix sqlserver tests

* fix test failures

* fix sqlserver test errors

* clean up

* kondo, test fix

* small changes

* remove mbql-clause and mbql-clause-with-opts

* make fixes

* more fixes

* remove new mbql5 usages

* Require metabase.driver.sql.pivot so the [:sql :pivot] emitter registers

Removing the sql-mbql5 driver moved sql_mbql5/pivot.clj to sql/pivot.clj,
but the side-effect require moved nowhere: on master
metabase.driver.sql-mbql5 required its pivot namespace, and after the move
no production namespace loaded metabase.driver.sql.pivot at all. Without
that load the apply-top-level-clause [:sql :pivot] method never registers,
so native pivot queries compile to a plain GROUP BY with no pivot-grouping
column (breaking pivot results at runtime). Whole-suite CI masked this
because loading the pivot namespace's own test file registers the method.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* lower ratchets

* fix ratchets whitespace

* use map over mapv

* remove oracle :value

* fix bigquery

* add back comments, update change log

* move comments

* remove unneeded mbql4 cases

* move pivot to sql

* destructure after clause check

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:44:13 -04:00
Stas Gavrylov
a7c0de1718 GDGT-2938: Unify spinner UI in Monitor grids (#79303)
* GDGT-2938: Review fixes

* Keep the broken-dependents column wide enough for its header

Capping the column truncated the 'Broken dependents' header, which turned on
the Ellipsified tooltip and left two copies of that text in the DOM once the
header was clicked. Its cells hold a short count, so auto width measures the
header and is already stable across pages.

* GDGT-2938: Address PR comments
2026-08-04 17:26:36 +03:00
Denis Berezin
0dc220840e UXW-4717 Display as dropdown options are cropped (#79319)
* UXW-4717 Display as dropdown options are cropped

* Remove comments
2026-08-04 17:22:41 +03:00
Chris Vandoodewaard
265aa21a24 cleanup unused files in frontend/test (#78232)
* remove unused mantineSelect

* cleanup unused files
2026-08-04 08:56:00 -04:00
Alexander Polyankin
6908f26b76 Fail loudly in dev and test when permission checks run one query per entity (#79244)
* Load permission caches whole, with tables in a RoaringBitmap

Each of the three request-scoped permission caches now loads every
database on its first miss, instead of loading the scope it was asked
about and tracking which scopes it covers. That removes the reason the
prime functions existed: a caller no longer has to declare what it is
about to check, so `prime-table-perms-cache`, `prime-db-perms-cache` and
`prime-schema-perms-cache` are gone along with their twenty-odd call
sites, and with them the whole class of bug where a new call path silently
becomes a query per table.

Loading everything is only affordable because the table cache stops
storing a value per table. It now keeps, per permission type, one
RoaringBitmap per permission value holding the IDs of the tables that
resolve to it — a partition, so a lookup walks the type's values and stops
at the one that matches, and each table ID is stored once. Table IDs are
unique across databases, so nothing is keyed by database any more, and
only tables with a grant of their own appear at all.

The bitmaps are built by mutation while private to the load and never
written to once published.

Uncached callers — a check on a user other than the requester, or inside
`disable-perms-cache` — still load just the one table or database they
ask about, since no later check will amortize a wider load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Only use a permission cache that a request scope actually bound

The three caches are dynamic vars, and `use-cache?` checked only that the
user being asked about was the requester -- not that anything had bound a
fresh atom. Code running outside `with-relevant-permissions-for-user` read
and wrote the JVM-wide root atom.

That was survivable while a cache tracked which databases and tables it
covered, since an unseen one still forced a load. Now that a load covers
everything, the first check for a user writes "all of it, for good" into
an atom with no lifetime, and every later check for that user in the same
JVM is answered from it -- so a permission granted afterwards is never
seen. Tests, which set `*current-user-id*` freely outside any request
scope, poison it on the first check.

The root binding is now nil and `use-cache?` takes the cache, so an
unbound one reads through to the database. This is what bounds the
staleness each cache's docstring promises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Record which databases a cache load covered

A cache that loads everything cannot tell "the user has no rows for this
database" from "this database did not exist when I loaded", and answered
both as no-permissions. Callers fall back to the least permissive value,
so a database created inside a request scope came back `:blocked` --
which is what broke the driver and app-db suites: `mt/dataset` builds a
database inside the scope `mt/with-test-user` has already opened, and
every check against it was denied.

Each cache now stores, per user, the set of database IDs its load
covered alongside the index. A question about a database outside that
set reloads. The set is read before the load, so a database appearing in
between counts as uncovered and forces a reload rather than being
wrongly marked covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Revert the bitmap cache, back to scoped permission caches

Loading every database on a miss traded per-call scoping for a cache that
then has to prove what it covered, and it cost two correctness bugs of
the same family -- the cache over-claiming, first across time (no
lifetime) and then across databases (no coverage record). The memory win
did not pay for that surface.

Back to master's caches, which load the table or database they were asked
about and record it. The next commit adds what this branch was really
after: making an unprimed check loud instead of silently expensive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add strict cache-miss detection and prime the paths it found

Adds *perms-cache-misses-are-errors?*, bound true by
with-relevant-permissions-for-user in dev and test. A cache's first load
is ordinary; a later miss means an earlier load already covered a
different scope, which is the signature of an unprimed run.

prime-table-perms-cache now also primes the db cache and takes :db-ids,
since table-permission-for-user reads the database's own grant too, and
call sites pass :db-ids wherever they have them.

Genuine N+1s this found and fixed: readable-fields-only, metric
dimension filtering, and the schema listing's table filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Throw on the fifth cache miss, not the second

Measured across the search, sandbox, dashboard and sync suites: at a
limit of three, 30 tests fail; at five, 18; and raising it to eight or
twelve changes nothing. So the cases between three and five are bounded
-- a dashboard whose parameters span two databases, an entity read plus
a chain-filter elsewhere -- and everything still caught at five is an
unbounded run. MB_PERMS_CACHE_MISS_LIMIT overrides it for re-measuring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the miss-limit env var, keep the measured constant

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Correct the miss-limit measurement, revert the unfounded search prime

The 18 failures I measured against were pre-existing: the same batch on
the base commit produces them identically, so the apparent plateau from
five upward was those failures, not the limit catching anything.

Re-measured against that baseline: a limit of two flags 19 sites and
three flags 10, all bounded -- an entity read plus a chain-filter, a
dashboard spanning two databases -- and at four and above none flag. Five
stays, but as a backstop against runs that grow with the data rather than
a linter for every second check.

Also reverts the search prime, which was added on the assumption those
failures were permission misses. They were not, and it bought a query per
search for nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Prime the db perms cache for search's per-table permission checks

`check-permissions-for-model` resolves `table` and `indexed-entity`
results against their database, so a search spanning N databases cost N
queries. Results are consumed as a stream, so the databases involved are
never known up front -- and a search ranges over all of them anyway, so
prime every database.

Caught by the strict cache-miss check: metabase.search.api-test's
permissions-test failed with 24 database-level misses, all from
search.impl/can-query?. Postgres-only because the appdb search engine is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Order broken pulses by id so notification order is deterministic

`broken-pulses` selected with no ORDER BY, so the order the notifications
went out in was whatever order the rows came back in.

handle-broken-subscriptions-due-to-bad-parameters-test asserts the second
email is the one about the slack pulse, so it fails whenever the two
broken pulses come back the other way round. Shard-dependent, since the
physical row order shifts as other tests churn the table -- it does not
reproduce standalone, and it fails on master too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Exclude destination databases from full-database selects

Destination databases are reachable only through their router and never
carry data_permissions rows of their own, so listing them just widens the
load. The permission graph in permissions_rest already filtered them;
admin-permission-graph and the new search prime did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Prime table perms, not just db perms, for search's table results

A `table` search result is checked with `can-query?`, which goes through
table-permission-for-user -- and that reads the table's own grant as well
as its database's. The database half short-circuits only when the db-level
grant is already maximally permissive, which is why the CI failure showed
database misses alone; a user with table-level grants would have gone on
to miss the table cache once per result.

prime-table-perms-cache covers both caches. Passing only :db-ids keeps it
affordable: the table cache holds data_permissions rows carrying a
table_id, so the load is bounded by the user's table-level grants rather
than by how many tables exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Point the cache-miss error at one prime function and one entity

The message listed all three prime functions and the ex-data carried only
a kind, so it said a check was slow without saying which check.

cached-database-perms takes the table-id its caller was checking, when it
has one. A miss reached that way is a table check -- table-permission-for-user
reads the database cache on its way through -- so it now reports kind
:table and names prime-table-perms-cache, which primes both halves;
prime-database-perms-cache would have left the table half unprimed.

The kind -> function map is spelled out rather than built from the kind, so
renaming one of those functions turns it up. Also renames db to database in
the function names touched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Rename full-db-permission-for-user to full-database-permission-for-user

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Batch the db-ids table-perms load, and stop formatting ids as numbers

Two review findings, both real:

prime-table-perms-cache batched only its :table-ids branch. The :db-ids
branch passed the whole set to one statement, and search now hands it every
non-destination database, so a large enough instance would blow the
bound-parameter cap. Batched like its sibling and like
prime-database-perms-cache.

The miss message went through tru, so MessageFormat rendered table 1234567
as "table 1,234,567". It only ever throws in dev and test, so it does not
need translating -- plain format, no grouping. The test used id 42, which is
below the grouping threshold and so passed against the broken format; it now
uses ids past the thousands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 08:29:10 -04:00
Oleksandr Yakushev
521b4eb271 [test] Use pre-created H2 DB in metabase.query-processor.middleware.cache-backend.db-test (#79277) 2026-08-04 12:06:18 +03:00
Fraser
3550513bf9 Move DocumentMode into rich_text_editing (#79345)
Move DocumentMode next to its consumer in rich_text_editing
2026-08-04 19:14:48 +12:00
Fraser
630ccf8acc Fix boundary-stats spec fixtures for enforceSharedTiers (#79348) 2026-08-04 16:23:27 +12:00
Fraser
91cec8cad5 Add sub-tiers for shared modules (#79013) 2026-08-04 12:21:28 +12:00
Fraser
616ee101b7 Dev 2343 record coverage per test and track routes (#78195) 2026-08-04 12:07:22 +12:00
Jeff Bruemmer
e88986d08c docs - update lychee config (#79316)
* update lychee config

* bump lychee action
2026-08-03 15:41:11 -06:00
technomancy
f11098bf5c Add migration to fix query execution primary key name. (#79324)
The query_execution_partitioned migration failed on stats because the primary key
constraint had an inconsistent name; it originally came from h2 so it didn't match
the constraint name we saw in development. This migration should do nothing unless
the DB is in the same situation as stats where it has a bad constraint name.
2026-08-03 21:24:27 +00:00
Nemanja Glumac
0bbd9658b0 Assert absent connection-spec keys instead of using complement (#79330)
`=?` applies an expected function to the actual value, so `complement` --
`clojure.core/complement`, the higher-order function that negates a
predicate -- evaluates to `(complement <actual-value>)`, which returns a
new function. Functions are truthy, so those checks passed for every
input and asserted nothing. `:hawk/key-not-present` is the assertion the
lines were reaching for: the key is absent from the spec.

Hawk also stopped feeding absent keys into the expected predicate as a
truthy sentinel, and now reports a missing key as a diff outright, so
the remaining checks over keys that are not present no longer pass
vacuously either.

In `can-change-from-password-test` that exposed two assertions the test
DB could never satisfy: it authenticates with an uploaded private key
and sets `use-password false`, so `:password` was absent from both the
details and the resulting spec, while the first two cases assert that a
password is present and takes precedence over a key file. The details
now carry a password to switch away from, which is what the test is
about.

In `normalize-use-password-test`, `normalize-details` only ever assocs
`:use-password true`, and only when no private key is configured. The
DB under test has a private key id, so the key is never added -- absent
rather than false.
2026-08-03 21:12:41 +00:00
Andrei Simionescu
18673f52d4 Add andreis to Metabot team and slack map (#79323) 2026-08-03 21:41:30 +01:00
Oleksandr Yakushev
507db4b36f [test] Speed up metabase.util.queue-test (#79278) 2026-08-03 22:30:44 +03:00
Fraser
1c81a02962 Remove user slice (#77672) 2026-08-04 07:26:45 +12:00
metamben
9d12bffcd5 Fix test: send broken-pulse notifications in order (#79321)
The Postgres 14.x (Driver Tests) suite is broken because a test
expects the notifications to arrive in order.
2026-08-03 17:44:25 +00:00
Kamil Mielnik
245af1e945 Custom viz SDK 2.0 (#78354) 2026-08-03 17:31:54 +00:00
Chris Truter
acba9a1d24 Use scoped credentials for MCP UI (#79312)
* Validate OAuth token scopes for MCP

* Test MCP tool calls are refused outside token scopes

* Clean up scoped MCP token test

* Use scoped credentials for MCP UI

* Use MCP public API for UI credentials

* Fix credential test regex, auth-method arity test, reflection warnings

- api_test: the uiCredential extraction regex was over-escaped (matched
  literal backslashes) and could never match the rendered HTML, so
  mcp-ui-credential-validation-test failed on every backend job
- middleware session_test: auth-method grew a 5th arg (mcp-ui-info) but
  auth-method-test still called it with 4, throwing ArityException; also
  cover the new "mcp-ui" auth method
- session.clj: type-hint the signature comparison so Eastwood's
  reflection check passes
- align let/map bindings per style guide; mention the MCP UI credential
  path in wrap-current-user-info's docstring
- frontend: scope the no-literal-metabase-strings disable to the header
  line with a reason; drop now-unused setSessionTokenHeader and update
  stale comments that referenced it

---------

Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
2026-08-03 11:14:09 -06:00
Chris Truter
bb7e944a22 Test OAuth token scopes for MCP (#79307)
* Validate OAuth token scopes for MCP

* Test MCP tool calls are refused outside token scopes

* Clean up scoped MCP token test
2026-08-03 17:10:48 +00:00
Eric Normand
b160246754 Keep the git sync cancellation error toast up for a minute (#79320)
The "Failed to cancel sync" toast used the app-wide 5s default, which is
too short to read and screenshot, especially since the same handler
dismisses the sync modal underneath it.

Fixes #79029
2026-08-03 16:40:54 +00:00
Anderson
b84036a312 Render pinned metrics preview charts consistently in collections (#79090)
* Fix pinned metric previews in collections

* Address review comments

* rename clj function, add comment
2026-08-03 13:28:22 -03:00
Stas Gavrylov
0eb9336cc6 BOT-1937: Restore clickable tool calls in Metabot debug chats (#79318)
BOT-1937: Restore the tool call details popup in Metabot chats

Moving the AI analytics views to Monitor replaced the self-contained tool
call modal with an onSelect callback, wired up only by the Monitor
conversation detail page. Normal Metabot chats pass no callback, so
clicking a tool call in a /debug chat did nothing.

Bring the modal back for callers that don't render details themselves,
composed from the existing ToolCallTitle and ToolCallDetailsContent so
the chat and the Monitor sidebar keep sharing one details renderer.
2026-08-03 18:28:17 +03:00
Nicolò Pretto
a747e0edb1 automatically enabled the custom viz used in entity in the embed wizard (#79183) 2026-08-03 14:55:31 +01:00
Anderson
ea64c1e6f6 Discard pending slice name edits when pressing Escape (#78690)
* Fix chart settings popover Escape cancellation

* Address review comments
2026-08-03 10:45:48 -03:00
Stas Gavrylov
a7691ffcbf GDGT-2931: Make Alerts Management tabs visible by default (#79166)
* GDGT-2931: Make Alerts Management tabs visible by default

Removes the full-page blocking loader on Alerts Management so tabs,
filters, and search render immediately, matching the pattern used by
Dependency Diagnostics and CLI/MCP analytics. The grid now shows an
animated skeleton while loading instead of blocking the whole page.

All three tabs (All/Failing/Ownerless) always render with a live count
badge instead of hiding tabs with zero results.

* Add positive assertion for the tab count loading skeleton

Give the skeleton badge a test id so the loading-state test can assert
it's actually rendered, not just that the count text is absent.

* Drop the visually-hidden loading status for the table skeleton

Reverting a restore I mistakenly applied earlier — this simplification
was an intentional manual edit, not something to undo.

* Remove the tab-switch refetch test and its listGate scaffolding

* GDGT-2931: Address PR comments
2026-08-03 16:18:53 +03:00
Kamil Mielnik
b8fe5f8247 GDGT-2875 Running a python transform without any table selected returns an unknown error (#79178) 2026-08-03 18:04:48 +07:00
Fraser
73e5cfb0f5 Move dashboard click-behaviour code into dashboard (#79145) 2026-08-03 22:09:27 +12:00
Romeo Van Snick
dd70db787e Import metabase/router through its single entry point (#79298) 2026-08-03 18:46:35 +10:00
Kamil Mielnik
adecad50c1 GDGT-1782 Timezone detail is missing when Setting up Subscriptions (#79171) 2026-08-03 14:41:24 +07:00
Romeo Van Snick
ffc964cd53 Move the app's link onto react-router's NavLink (#79296)
* Export react-router's link primitives from metabase/router

* Move the app's link onto NavLink and drop the v3 active-link props
2026-08-03 17:22:44 +10:00
Romeo Van Snick
8f68857a67 Drop the last v3 parity behaviours in metabase/router (#79294)
* Use react-router's own <Navigate>

* Interpolate redirect targets with generatePath and resolve them like v7

* Justify queryToSearch's encoding on its own terms, not history@3's
2026-08-03 16:35:11 +10:00
Fraser
27a7dc50e6 Enforce module public APIs (#79292) 2026-08-03 18:16:10 +12:00
Romeo Van Snick
452f689750 Author the app's routes as data routes (#79268)
* Author the app's routes as data routes

* Render route objects in specs through a renderRoutes helper
2026-08-03 15:27:52 +10:00
Romeo Van Snick
48a01bd3b0 Collapse the metabase/router facade onto react-router (#79242)
* Re-export react-router from the barrel instead of a file each

* Flatten the v7 folder into metabase/router

* Drop the V7 suffixes and merge the two router providers

* Use react-router's own hooks and types instead of mirroring them

* Drop the dead function form of a Link target
2026-08-03 14:30:01 +10:00
Fraser
b294a66b56 Record video on e2e re-run attempts (#79279) 2026-08-03 15:47:36 +12:00
Fraser
d9064c454a Use subscription-appropriate error fallback in pulse actions (#79113)
The subscription save path borrowed getActionErrorMessage from
metabase/actions/utils, whose fallback text ("Something went wrong
while executing the action") is wrong for this call site. Call
getResponseErrorMessage from metabase/utils/errors directly with a
subscription-specific fallback, and drop the actions import.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 15:47:08 +12:00
Romeo Van Snick
947c11579b Delete the Route translation layer (#79151) 2026-08-03 10:37:55 +10:00
dpsutton
6fcdf94fa5 Put CI MySQL/MariaDB datadirs on tmpfs (#79226)
* Put CI MySQL/MariaDB datadirs on tmpfs

A full app-db migration chain on a stock mysql:8.0 container is ~59s of
which ~44s is fsync waiting: MySQL 8 images default to full durability
with the binlog on (log_bin=1, sync_binlog=1, flush_log_at_trx_commit=1),
every DDL statement implicitly commits, and DDL additionally fsyncs the
table files it creates — one measured setup-db! run performed ~50k data
fsyncs + ~37k redo-log fsyncs across 1,464 changesets. No statement is
hot; it is a uniform ~30ms/changeset disk tax, paid for a database CI
deletes minutes later.

Durability knobs cannot recover it: the dynamic ones
(flush_log_at_trx_commit=2, sync_binlog=0) win back only ~20s because DDL
data-file fsyncs are not governed by them, and --skip-log-bin needs a
server command-line flag GHA service containers cannot pass. --tmpfs fits
the one field they do accept (options:) and neutralizes every fsync class:
measured 59.3s -> 15.8s per setup-db!, reproduced.

Applied to every mysql/mariadb service container (app-db, drivers,
semantic-search, cross-branch-migrations). Expected effects: the :db init
timeouts that outran #79014's 120s bump within a day become structurally
impossible (~5-16s init), the ~25min of Liquibase inside a mysql app-db
job mostly evaporates, and job duration stops tracking runner disk
contention. Cost: the app db lives in runner RAM (test DBs are MBs) and
images with pre-baked datadirs re-initialize at container start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCo9W7tgK13nNSfWTtQoUc

* Add required description to the cross-branch-migration-test action

Pre-existing actionlint error surfaced by touching
cross-branch-migrations.yml (actionlint scopes to changed files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCo9W7tgK13nNSfWTtQoUc

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 10:27:25 -05:00
Oleksandr Yakushev
51bc6d11ac [test] Optimize download-exports-test/pivot-with-scale-test (#77066) 2026-08-01 13:59:13 +03:00
Oleksandr Yakushev
37fccf450c Bump Kondo version (#78647) 2026-08-01 11:33:41 +03:00
Arik Aronov
b7330b60a4 Resolve snippet template tags subject to read permissions (#78905)
* Resolve snippet template tags subject to read permissions

* snippet read permission test

* fix kondo module config
2026-08-01 11:29:53 +03:00
Romeo Van Snick
bd1d5d08ef Run the route-leave guards on native useBlocker (#79149)
* Run route-leave guards on native useBlocker

* Chain the route-leave guards and scope the registry to the provider

* Keep the prompted destination when another navigation supersedes it

* Keep the held navigation in the ref, not resynced from state

* Resume the captured navigation instead of reissuing it

* Revert the supersede handling, keeping the guard fan-out

* Stop the dashboard syncing its URL while a leave prompt is up
2026-08-01 09:54:22 +10:00
Alexander Polyankin
f94fc55418 Cut per-request permission work: consolidate caches, prime table checks, drop the visible-collection set (#79197) 2026-07-31 23:15:52 +00:00
Andrei Chernykh
563476b207 chore: Convert metabase-lib/v1 metadata utils to TypeScript (#79072)
* Convert metabase-lib/v1 metadata utils to TypeScript

* Address review: reuse shared template-tag mock, tighten schemaName type, restore id-null test case, hoist virtual-table id lookup
2026-07-31 15:57:31 -07:00
Nemanja Glumac
4b69a5a61d Remove Trunk (#79199) 2026-07-31 23:38:29 +02:00
Oleksandr Yakushev
5918cc9a6b [test] Optimize modules-test (#77065) 2026-07-31 23:44:41 +03:00
technomancy
c28f952435 Partition the query_execution table when using Postgres AppDB. (#78497)
* Partition the query_execution table when using Postgres AppDB.

The `query_execution` table gets very big on large installs and poses
performance problems. Partitioning it will allow us to get those under control.
This will only apply when using Postgres as the AppDB. If you have an H2 AppDB,
it's probably not big, and if you have a MySQL AppDD, then, uh, well, too bad.

The basic plan is to rename the existing `query_execution` to
`query_execution_old` and then create a new `query_execution` partitioned table
with the same schema. Once that's done, we can attach `query_execution_old` as
a partition to the main table, and all the data should be available transparently.

Unfortunately partitioned tables have gotchas that complicate things. This
means the migration plan is split into different steps.

The partitioned table must not have a primary key that doesn't include the
partition key. In order to do this, we have to stop using `id` as the primary
key and replace it with a composite key of `id, started_at`.

The first migration swaps the primary keys around, dropping the ones
based on the id alone and replacing them with ones based on `id` and `started_at`.

The second migration renames `query_execution` to
`query_execution_old` and then creates the new `query_execution` table
which is based on `query_execution_old` but partitioned by
`start_at`. Then it takes the `query_execution_old` table and attaches it as a
partition into the full `query_execution` table. This partition covers up to
the current month, plus one more after, in order to handle the case where the
migration runs at the end of the month and query executions need to be written
to disk before the job to create the next partition gets a chance to run.
The table's IDENTITY has to be dropped since this can't be done in a partition;
it should be handled by the higher level table.

The v_query_log view references the old query execution (now `query_execution_old`)
table. We need to re-create the view to point it at the new table instead.

Finally a migration dynamically calculates a new name for `query_execution_old`
to turn it into `query_execution_2026_08` or whatever, depending on
the date of the data that the table will actually contain.

* Add a metabase.app-db.partitions jobs for managing old/new partitions.

The migration puts us in a good state for the present, but as time
goes on, we'll need new partitions for new months. The new
`metabase.app-db.partitions` namespace handles this by defining a new
`ManagePartitions` job that runs daily to see if any new partitions
need to be created or whether any existing partitions need to be
detached.

Each run checks the current month's partition and the next month's
partition and creates them if they haven't been created yet. It also
checks for the retention cutoff period to see which partitions to
detach.

The individual logic functions and the `manage-partitions` function
that tie them together are tested, but the `defjob` is not; I don't
know if there is a way to test jobs.

There is no mechanism for actually deleting the old data once it
passes out of the retention period; it merely becomes detached. We may
need to document this somewhere so that users know they can clean it
up to save disk space. (It's unclear to me where that would be
documented.)

* Disable truncate-audit-tables on query_execution when using postgres.

* Address review suggestions.

* Set sequence on new partitioned table to match old sequence value
* Ensure partitioning job is loaded at startup
* Fix off-by-one error in partition boundaries for dropping
* Use locale-independent yyyy year formatting
* Move to metabase.audit-app.task.partitions

Still needs to address the fact that rollback only converts a single
table instead of all partitions back into one table.

* More review suggestions:

* Fix rollback sequence restoration to target old identity sequence.
* Fix rollback of table renames to use the oldest partition.
* Make partition management cron fire immediately in cases when it's been missed.
* No need to export retention days setting in audit-app module.

* Don't recreate primary key with id, started_at; just drop the primary key.

As far as I can tell, the primary key is not used any more since the
field_usage table got dropped; all reads of this table are just counting.

* Drop partitions instead of just detaching when cycled out of retention.

* Fix rollback to drop view first, then recreate at the end.

* Avoid relying on max(id) in partition migration.

The index will be gone by the time this runs, so it would result in a
full table scan of a HUUUUUUGE table.
2026-07-31 13:25:16 -07:00
Oleksandr Yakushev
2221279f6f [test] Don't attach unrolled query args to thread context (#77580) 2026-07-31 23:24:07 +03:00
Oleksandr Yakushev
3c1615f815 [malli.registry] Try look up schema in cache without de-regexping it (#78099) 2026-07-31 23:23:01 +03:00
metamben
c991b6d3f7 Compare ex-data, ex-message of throwables on pivot parity mismatch (#79180)
This is a follow-up to #78843.
2026-07-31 19:25:08 +00:00
Nemanja Glumac
6c519afd69 Wire the minimal-tests to the ci-conductor quarantine (#79160) 2026-07-31 20:47:40 +02:00
Ben Grabow
f85ec40f72 Use explicit hierarchy for events (#78971) 2026-07-31 13:31:37 -05:00
Jeff Bruemmer
789e16832c docs - embedding a chart (#78856)
* draft

* note

* image and links

* interactive

* intro table

* edits

* various edits and link fixes

* edit

* Optimized 1 images, saved 247.2 KB (43.7% smaller)

* Apply suggestions from code review

Co-authored-by: Alex Yarosh <alexandra@metabase.com>

* Apply suggestions from code review

Co-authored-by: Alex Yarosh <alexandra@metabase.com>

* query

* images

* edits

* intro

* question

* remove table

* intro

* edits

* edit

* Apply suggestions from code review

Co-authored-by: Jeff Bruemmer <jeffbruemmer@gmail.com>

* question reference

* revisions

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: Alex Yarosh <alexandra@metabase.com>
2026-07-31 13:52:26 -04:00
Nemanja Glumac
4f4264a011 Bump hawk to 1.0.18 (#79070)
* Bump `hawk` to `1.0.18`

This corresponds to
c17e2b9b4e

* fix bad tests

* select data key

---------

Co-authored-by: Riley Thompson <rileythompson99@gmail.com>
2026-07-31 17:51:25 +00:00
Arik Aronov
8b8d31b5d2 card creator field selection (#79065) 2026-07-31 20:45:54 +03:00
metamben
e0d007399f Set explicit default binning when "Don't bin" is specified (#76930)
* Set explicit default binning when "Don't bin" is specified

Closes QUE2-91
2026-07-31 17:38:08 +00:00
Nemanja Glumac
71fd271cc6 Retry the quarantine list fetch before giving up (#78951)
* Retry the quarantine list fetch before giving up

A dropped socket or a 5xx from ci-conductor made the gate report COULD NOT
CHECK on the first try. Transport errors, timeouts, 429s and 5xx now get three
attempts with exponential backoff and jitter; a 4xx returns straight away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* improve retry api

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ryan Laurie <iethree@gmail.com>
2026-07-31 17:21:44 +00:00
Timofey Kachalov
45bf409390 Improve errors when data app iframe is loaded without a parent or when iframe can't be loaded (#79169) 2026-07-31 19:57:38 +04:00
github-automation-metabase
1d78177e7c chore(sdk): bump SDK version on master to 0.64.0-alpha.2 (#79174)
Co-authored-by: Timofey Kachalov <timofei@metabase.com>
2026-07-31 14:44:21 +00:00
metamben
335773bf84 Enable native pivot for easy native pivot candidates (#78843)
* Enable native pivot for native pivot candidates
* Add setting the optionally enable native pivot processing
2026-07-31 17:06:14 +03:00
Sébastien
5cd3310e0e Use the MCP icon for MCP analytics in Monitor nav (#79161) 2026-07-31 15:34:28 +02:00
Nicolò Pretto
987f30803a add support for custom viz in EAJS (#78133)
* Add allowedCustomVisualizations setting to EAJS embeds

This commit adds the plumbing necessary to pass down the
value of the allowlist for custom viz.
It warns when wrong names are passed (ie: not starting with `custom:`),
and forces [] for guest embed as the feature is not supported there.

* Move sandbox-host to its own namespace to remove +auth from it

The donor document is loaded as an iframe src, which cannot carry session
headers, so the endpoint must be reachable without authentication. Auth is
applied per-namespace by ns-handler in custom-viz-plugin.api and cannot be
lifted for a single endpoint, so the endpoint moves verbatim to its own
namespace, mounted without +auth next to the session-authed routes (both
still behind the :custom-viz premium gate). No behavior change other than
dropping the auth requirement.

* Serve the custom viz sandbox donor to EAJS embeds
Makes the donor iframe-able as long as either a valid session is passesed
or the correct sec-fetch-site headers are passed.

* Allow blob: images on the EAJS embed page for custom viz icons

Custom viz icons in the chart type picker are served by the SDK asset
manager as blob: URLs (a cross-origin img cannot carry the session
header, and EAJS has no cookies). The modular embedding page runs on the
Metabase origin under a strict img-src that omits blob:, so the browser
blocked the icons and left a broken image in the picker. Add blob: to
img-src only for the /embed/sdk/ entrypoint, gated on a URI predicate so
the main app and static or public embeds keep the stricter policy.
2026-07-31 15:21:05 +02:00
Mahatthana (Kelvin) Nomsawadi
2c8d1ad694 chore: tidy the SDK release dispatch forms and bump output (#78999)
* chore: tidy the SDK release dispatch forms and bump output

The dispatch form renders an input's description as the control's label,
which is a single plain text node: newlines collapse to spaces and no
markdown is parsed. Both SDK release workflows had multi-line
description blocks written as if they rendered, so release_type's
occupied ten rendered lines and pushed the select below the fold.

- release_type: one-line description keyed by option, options reordered
  into release-cycle order, and default: alpha pinned so the reorder
  does not move the preselection. Option values are unchanged.
- Detail that no longer fits the form moved to the file header.
- Shortened the two read_first option strings that overflowed the
  collapsed select at 44 and 81 characters.
- Gave the version-bump PR body's table a real header instead of an
  empty one (EMB-2167).
- Capture the new PR's URL and append it to the job summary; the
  existing summary step runs before the PR exists (EMB-2168).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo

* chore: capitalise release type names in the dispatch label

The label renders as plain text, so no markdown emphasis is available.
Caps makes each option findable when scanning the wrapped paragraph.
Values stay lowercase - this is display emphasis only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo

* chore: say what each dispatch input is for, not what it isn't

prerelease_id read as an output description, so it never said what to
type. Phrase it as a mapping: enter data-apps, get 0.62.5-data-apps.0.
Same length, same six rendered lines.

The release workflow's label opened with a double negative - use it
'when neither the bump PR nor re-running the failed jobs will do it' -
which left the reader to infer the purpose. It is a manual override, so
say that first. Retry guidance stays in the file header, where there is
room for it. Eight rendered lines down to six.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo

* chore: drop a redundant comment above the dispatch label

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:12:08 +01:00
Sébastien
2627e335f3 Record CLI usage analytics from the REST API middleware (#79152) 2026-07-31 14:37:43 +02:00
github-automation-metabase
d5a43a3a3a Auto-update documentation for master (#79102)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-31 07:59:56 -04:00
YB
1c4ea2115b [GDGT-2940] Upgrade libthrift to 0.24.0 (#79148)
Dependency upgrade: align both drivers that ship org.apache.thrift/libthrift on the latest release (0.24.0).

hive-like: bump the existing direct pin 0.16.0 → 0.24.0. Since 0.19.0, libthrift declares runtime deps (httpclient5, httpcore5, jakarta.servlet-api) that only serve its THttpClient/TServlet classes; hive-jdbc is built against httpclient 4.x (already pinned in this module) and cannot use them, so they are excluded to keep the driver jar lean. Verified by scanning every class in the built jar: the only classes referencing those packages are libthrift's own five HTTP/servlet classes - nothing in the hive/hadoop stack touches them.
databricks: pin 0.24.0 over the 0.23.0 bundled with databricks-jdbc-thin 3.4.1, following this module's existing pattern of pinning newer transitives (lz4, bouncycastle, jackson, commons-configuration2).
Previously the two driver jars bundled different libthrift versions (0.16.0 and 0.23.0) into the shared plugin classloader; this aligns them on a single version.

Also updates the Spark SQL connection form's "additional JDBC options" placeholder from ;transportMode=http to ;transportMode=binary - binary is the default and supported transport; hive-jdbc's http transport mode is not compatible with libthrift ≥ 0.19 (it passes an httpclient 4.x object to a constructor that now takes httpclient 5.x, see #46793), so the form should not suggest it.
2026-07-31 19:47:33 +08:00
YB
665063e39f [GDGT-2939] Update lz4-java to 1.11.1 in clickhouse and databricks drivers (#79147)
Routine dependency upgrade: bumps the pinned at.yawk.lz4/lz4-java (maintained fork of the discontinued org.lz4:lz4-java) to the latest release, 1.11.1, in the ClickHouse and Databricks driver modules.

Upstream changes between the pinned versions and 1.11.1 are additive only (new fast-reset compressor classes, a linux-riscv64 native binary, bugfixes) — no classes removed, same net.jpountz packages, so it is a drop-in update for both JDBC drivers. Neither driver module calls the library directly.
2026-07-31 19:47:22 +08:00
Timofey Kachalov
fedf6fbe01 Prevent requests with undefined collection id (#78822) 2026-07-31 14:41:25 +04:00
Chris Truter
5426e9a50a Avoid deadlocks when deleting databases without fields (#79048) 2026-07-31 09:52:35 +00:00
Stas Gavrylov
b34f244ffa GDGT-2874: Move AI analytics to Monitor (#78532)
* GDGT-2884: Move AI analytics to Monitor (#78222)

* GDGT-2874: Relocate AI analytics source under monitor/ai-auditing

* GDGT-2874: Add Monitor URL helpers for AI Auditing

* GDGT-2874: Repoint hardcoded legacy paths to Monitor URL helpers

* GDGT-2874: Swap MetabotAdminLayout for MonitorMain in AI Auditing pages

* GDGT-2874: Fix stale audit_app import in metabot-analytics test-support mock

Plan gap: this file wasn't in Task 1's moved-tree scope and wasn't listed as an
external consumer to fix in a later task. Caught during Task 4 verification
via type-check-pure.

* GDGT-2874: Add AI Auditing sections to monitor_section_clicked event

* GDGT-2874: Add canAccessAiAuditing selector and route guard

* GDGT-2874: Add combined AI Auditing routes getter and plugin index

* GDGT-2874: Move AI Auditing plugin wiring from PLUGIN_AUDIT to PLUGIN_MONITOR

* GDGT-2874: Mount /monitor/ai-auditing route block

* GDGT-2874: Add AI Auditing tab to Monitor nav

* Add regression tests for AI Auditing getActiveSection ordering

Lock in the order of the mcp/conversations/usage-stats route-matching
clauses in getActiveSection: the bare Urls.monitorAiAuditing() path
string-prefixes both sub-paths, so the more specific clauses must be
checked first (ts-pattern's P.string.startsWith is first-match-wins).
Without this, a future reorder would silently break active-tab
highlighting for the two sub-routes.

* GDGT-2874: Remove Auditing folder from Admin AI nav

* GDGT-2874: Add legacy redirects for /admin/metabot/usage-auditing

* GDGT-2874: Fix moved AI Auditing spec mocks and path assertions

* GDGT-2874: Repoint slash-command URL helper off the legacy admin path

The Metabot /inspect slash command still built its target URL via
metabase-enterprise/urls.ts's adminMetabotUsageAuditingConversation,
which hardcoded the old /admin/metabot/usage-auditing/conversations
path. It only worked because it bounced through the legacy redirect.
Delete that helper (its only caller) and use the canonical
Urls.monitorAiAuditingConversationDetail directly.

* GDGT-2874: Update E2E specs to canonical /monitor/ai-auditing URLs

usage-auditing.cy.spec.ts and mcp-analytics.cy.spec.ts predate the
Monitor migration and still visited/asserted on the legacy
/admin/metabot/usage-auditing* URLs, which only worked via the legacy
redirect. In-app navigation (row clicks, breadcrumbs, back button) now
lands on /monitor/ai-auditing* URLs, so any post-navigation URL
assertion in these specs was exercising stale paths. Point every
cy.visit()/pathname assertion at the new URLs, and add a small
regression test asserting the legacy URL still redirects, since the
main specs no longer exercise it directly.

* GDGT-2874: Apply AI Auditing cross-review fixes

* GDGT-2874: Add AI Auditing availability placeholders

* GDGT-2874: Gate AI Auditing pages with settings placeholders

* GDGT-2874: Keep unavailable AI Auditing links navigable

* GDGT-2874: Prioritize global AI settings for MCP availability

* GDGT-2874: Fix empty screen layout

* GDGT-2874: Use semantic AI Auditing navbar icons

* GDGT-2874: Restore Audit plugin ownership

* GDGT-2874: Test legacy redirects in app routes

* GDGT-2874: Add canonical Usage stats route

* GDGT-2874: Cleanup

* GDGT-2874: Address PR review feedback

- Rename AiAuditingAvailabilityLayout -> AvailabilityLayouts: the file
  exports two layout components (Metabot and MCP), neither named after
  the file/folder.
- Use !!useSetting(...) for the enabled/configured flags, matching the
  convention used everywhere else these settings are read.

* GDGT-2883: Restyle AI Auditing to match Monitor (#78356)

* GDGT-2874: Fix moved AI Auditing spec mocks and path assertions

* GDGT-2874: Apply AI Auditing cross-review fixes

* GDGT-2883: Restyle AI Auditing to match Monitor

* GDGT-2883: Fix inconsistencies

* Fix issues surfaced by restacking onto gdgt-2874

- Remove index.unit.spec.ts: orphaned artifact from a superseded
  gdgt-2874 draft, testing a ./index module that was never actually
  relocated here (registration logic still lives in audit_app/index.js,
  already covered by routes.unit.spec.tsx).
- Un-mock ConversationHeader in ConversationDetailPage.unit.spec.tsx:
  the blanket mock predated the conversation-title test and was
  hiding the real header it needs to assert against; the endpoint
  mocks setup() already provides make the real component safe to
  render.
- oxfmt fix in ConversationsTable.tsx.

* GDGT-2883: Fix Monitor layout consistency across AI Auditing pages

- Contain the Tool calls grid to the viewport (MonitorMain only clips
  when its parent is a flex container, so wrap it in a Flex h="100%"
  on the events tab; the charts tab keeps natural page scroll)
- Do the same for the Conversations grid page
- Hide the events table's pagination until it has loaded once, instead
  of showing a nonsensical "1 - 0 of N" range
- Add showTotal to the Conversations page's pagination, which was
  missing it entirely
- Match tabs/filters/content spacing (md) to Background Tasks on Usage
  stats and MCP analytics, which had drifted to lg
- Vertically center the ellipsis menu button against the conversation
  title in ConversationHeader

* GDGT-2883: Make all AI Auditing grid columns sortable, fix non-clickable cursor

- TreeTable: only show the pointer cursor on rows that actually respond
  to a click (onRowClick or getRowHref set), instead of unconditionally
  on every row. Fixes MCP analytics' Tool calls grid looking clickable
  when it isn't, and the same latent bug on several other display-only
  TreeTable consumers.
- MCP analytics Tool calls: wire up sorting for the remaining columns
  (ID, Client version, Tenant, IP address, Error type, Error message).
  The query builder already supported ordering by any valid view
  column, so this is purely frontend wiring.
- Conversations: add Title as a sortable column, including the backend
  allow-list/schema entry it needs. Queries and Searches stay
  unsortable — they're computed in-memory per page from message
  content, not real SQL columns.
- Fix a stale field name (summary -> title) in ConversationsPage's
  test mock that was failing type-check.

* GDGT-2883: Show tool call details in a sidebar on the Conversation Detail page

Replace the tool-call-click modal with a Monitor sidebar, matching the
rest of the AI Auditing views. The live Metabot chat widget keeps its
modal — AgentToolCallMessage now takes an optional onSelect callback
that, when provided, defers to the host instead of opening the modal.

Also style the sidebar's Request/Response copy buttons with the same
Copy/Copied! tooltip used elsewhere in the conversation view, and fix
a pre-existing bug where copying re-stringified the already-formatted
JSON text, copying an escaped string instead of clean JSON.

* GDGT-2883: Toggle the tool call sidebar closed on a second click

Clicking the same tool call record that's already selected now closes
the sidebar instead of re-opening it with the same content.

* GDGT-2883: Constrain conversation content to an 800px column, fix page scroll

Wrap the whole page body (header, stats, feedback, conversation, and
generated queries) in a single horizontally-centered 800px column
instead of leaving it full width.

Fix the page-level scroll along the way: the Flex wrapper added for
the tool call sidebar's width measurement gave MonitorMain's parent
display:flex, which made MonitorMain's flex+overflow:hidden CSS
actually take effect and clip content instead of letting the page
scroll. This page has no internal scroll container of its own (unlike
table-driven Monitor views), so drop the bounded height and let it
scroll naturally.

Also add the missing Copy/Copied! tooltip to AgentToolCallMessage's
own copy button, matching the one already added to the sidebar's
Request/Response copy buttons.

* GDGT-2883: Make AI Auditing pagination abortable, sync page in URL instantly

Every paginated AI Auditing grid (MCP analytics Tool calls, Conversations)
kept a stale in-flight request running when the user paged again before it
resolved, and the URL's page param lagged behind clicks by the shared
useUrlState hook's 300ms debounce — unlike Dependency Diagnostics, where
page changes are both abortable and instant.

- Swap McpEventsTable's and ConversationsPage's queries onto
  useAbortableQuery (the same hook DD already uses), so switching pages
  cancels the stale request instead of letting it complete in the
  background.
- Add an opt-in `immediate` option to useUrlState's patchUrlState that
  bypasses the debounce for a single patch, and use it for pagination
  clicks specifically. Filter/sort changes keep debouncing as before —
  this only affects callers that pass the new option.

* GDGT-2883: Sync pagination to the URL instantly across other Monitor list pages

Same delayed-URL-on-pagination-click bug as AI Auditing, found on a sweep
of every useUrlState consumer with a paginated grid: TaskListPage,
TaskRunsPage, NotificationsAdminPage, and ErrorOverview. Pass the new
patchUrlState immediate option on their pagination handlers too.

TaskListPage's pagination test asserted the old debounced timing (URL
unchanged immediately after a click, then advance the fake timer); updated
it to assert the new instant update instead. The other pages' tests only
asserted the eventual URL state, so they needed no changes.

* GDGT-2883: Use the empty-dashboard illustration for the disabled MCP analytics page

Swap the generic metabot icon for the EmptyDashboardBot illustration and
make the CTA a secondary button, matching other Monitor empty states.
Scoped to the MCP-disabled case only — the AI-features-disabled and
not-configured states elsewhere in this file are unchanged.

EmptyState gains an optional actionVariant prop (defaults to "filled",
preserving every other caller's current look).

* GDGT-2883: Apply cross-review a11y/correctness fixes

- Empty-dashboard illustration + secondary CTA now apply to all four
  AI Auditing unavailable states (AI disabled, not configured, MCP
  disabled), not just MCP-disabled — that was the intended scope; a
  narrower revert from a review pass was itself reverted here.
- Give the tool call copy buttons accessible names (were icon-only).
- Make TreeTable sort column headers keyboard-operable.
- Clear the selected tool call when navigating to a different
  conversation, so the sidebar can't reopen with stale data.
- Move focus into the tool call details sidebar on open and back to
  the trigger on close; label it as a region.
- Ignore a hidden/PII-gated sort column from a stale URL instead of
  querying by it.
- Add a Title-sort UI test and a one-shot immediate-bypass regression
  test.

* GDGT-2883: Fix code-review findings in TreeTable and AI Auditing

- Stop sortable header keydowns from bubbling to the treegrid, which
  could also activate a stale keyboard-focused row
- Give every populated TreeTable header the columnheader role, not
  just sortable ones
- Drop the redundant onRowClick push() on conversation rows, which
  double-navigated alongside the row's own link
- Render the MCP analytics loading/empty states inside the active
  Tabs.Panel so the selected tab's aria-controls stays valid

* GDGT-2883: Trim redundant comments in McpEventsTable and ConversationDetailPage

* GDGT-2883: Cleanup

* GDGT-2883: Fix e2e tests

* GDGT-2883: Address PR comments

* GDGT-2919: Migrate CLI usage analytics to Monitor (#78900)

* Project MCP events query to curated PII-aware columns

Restricts buildEventsQuery to exactly the columns the events table
displays, keyed off tenant/PII retention settings, instead of shipping
the view's full row (including ip_address/error_message) to the
browser regardless of the setting.

* GDGT-2919: Migrate CLI analytics to Monitor

* GDGT-2919: Drop ai-features-enabled? gate for CLI analytics

The gate was new, not preserved: the old admin route only checked
audit_app/admin access, with no ai-features-enabled? check, and CLI's
v_agent_api_calls data is independent of Metabot/LLM configuration.

* Fix oxfmt formatting

* GDGT-2919: Bring CLI analytics UI in line with the rest of Monitor

- Move the calls filter bar under the tabs and use MCP's spacing values,
  keeping tabs/filters mounted (and visible) through loading and empty
  states instead of swapping the whole Tabs block out.
- Replace CliEventsTable's AdminDataTable (admin-only, fixed
  maxBodyHeight) with TreeTable, matching McpEventsTable's structure so
  the Calls tab fills the available height like the rest of Monitor.

* Replace withRouteProps with useRouter across Monitor pages

withRouteProps is documented as a temporary v3-router migration shim
that reads location/params via useRouter() and re-injects them as
props. Have the Monitor pages call useRouter() directly instead and
mount them as plain route elements, dropping the wrapper.

* GDGT-2919: Make all Calls tab columns sortable

Matches MCP's events table, where every column (including the PK and
PII columns) is server-sortable. call_id/tenant_name/ip_address/
error_message are all plain columns on v_agent_api_calls, so ordering
by them needs no query changes beyond adding them to the sort list.

* GDGT-2919: Project CLI events query to curated PII-aware columns

buildEventsQuery shipped the view's full row (including ip_address and
error_message) to the browser regardless of the tenant/PII retention
settings; the frontend only hid those columns from the rendered table.
Add cliEventColumnKeys as the single source of truth for both the
query projection (Lib.withFields) and the display columns, mirroring
the same fix already applied to MCP analytics.

* Use absolute imports for cross-module paths in ai-auditing

Replace ../ relative imports (mcp-analytics/cli-analytics reaching
into metabot-analytics, and cross-directory imports within
metabot-analytics) with the metabase-enterprise/... absolute alias.
Same-directory ./ imports are left as-is.

* GDGT-2919: Show an error instead of spinning forever in CliEventsTable

useCliEventsQuery didn't surface the query's error, so a failed
per-page fetch left data undefined and the table stuck on the
skeleton indefinitely. Surface the error and render
LoadingAndErrorWrapper instead.

Also drop useRetainedValue: RTK Query's data (unlike currentData)
already stays sticky across page/sort changes until the next result
resolves, so the manual retention was redundant.

* GDGT-2919: Show an error instead of spinning forever in McpEventsTable

Mirrors the CliEventsTable fix: useMcpEventsQuery now surfaces the
query error, and McpEventsTable renders LoadingAndErrorWrapper instead
of leaving the skeleton up indefinitely on a failed fetch. Drops
useRetainedValue there too, for the same reason (RTK Query's data is
already sticky across page/sort changes).

Also trims redundant comments in both event tables.

* GDGT-2919: Address PR comments

* Revert gratuitous DependencyDiagnosticsPageOwnProps rename

Keep master's DependencyDiagnosticsPageProps naming so this Graphy-owned
file isn't touched by the master merge for no functional reason.

* Fix jest.mock("metabase/AppComponent") to match its named export

Master's router refactor changed AppComponent from a default-exported
connect() component to a plain named export (App). The mock still
returned a default export, so import { App } resolved to undefined and
broke every route render in routes.unit.spec.tsx.

* GDGT-2874: Fix usage audit e2e test
2026-07-31 12:42:13 +03:00
Fraser
3c59e2dacb Use viz-local VisualizationGridSize in treemap labels (#79017)
The treemap label model imported VisualizationGridSize from the
enterprise custom-viz package, a cross-boundary type import. A
structurally identical type already exists in
metabase/visualizations/types, so use that instead.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:32:19 +12:00
Timofey Kachalov
b75261aba2 Improve Data Apps sandboxing (#78938) 2026-07-31 11:37:43 +04:00
Romeo Van Snick
9a3add685a Host the app on the react-router data router (#78655)
* Split getRawBrowserHistory out of blocking-history

* Host the app on the react-router data router
2026-07-31 16:31:00 +10:00
Romeo Van Snick
aaada18ca8 Delete RouterBridge and collapse the router facade onto v7 (#79130)
* Resolve relative navigation against v7's own match

* Resolve redirect targets off the v7 match branch

* Drop the router argument from useDashboardUrlQuery

* Free Palette and McpAnalyticsPage from the router context

* Read location and params from v7 instead of useRouter

* Scope route-leave hooks from the match instead of an injected route

* Delete RouterBridge, the router contexts, and the v3 route types
2026-07-31 15:35:08 +10:00
Timothy S. Dean
5a232bb5c8 sql-jdbc: stop munging dashes in schema-qualified table identifiers (#78969)
HoneySQL munges dashes to underscore in a keyword's namespace, but not in its name. The code was relying on HoneySQL default behavior to turn namespaced keywords into table references, but the inconsistency bit me in some new development work.

Two paths currently pass schema-qualified keywords in cases where a table might leak:
- `drop-table!` targeted a nonexistent object and silently "succeeded" (not dropping the table)
- `compile-transform` (the SQL variant) could target a nonexistent database.

Both `drop-table-sql` and `create-table-sql` are updated (the latter via `quote-table`) to keep them in sync.

Collapsing the namespace into the keyword's name keeps everything verbatim as you'd expect. Callers already passing in `"schema.name"` weren't impacted.
2026-07-30 20:33:24 -06:00
Alexander Polyankin
e3e3a193f8 Fix QB perf (#79132)
* Fix QB perf

* Forbid loading every field via GET /api/database/:id/metadata

Make skip_fields a required `true` on the FE request type so no caller can ask
for field-level metadata, which grows with every column in the database.

The two callers that needed fields now scope their fetch to a single table:
- Data Reference field pages fetch the table's own query metadata (which also
  pulls FK target tables and their fields)
- useAuditTable resolves the table from the plain database response, then loads
  /api/dataset/query_metadata for that table

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Withhold the audit table until its fields load

ConversationStatsPage renders charts straight from useAuditTable's table
without checking isLoading, so handing back a table built from the field-less
database metadata made it paint once and re-render when the ad-hoc query
metadata arrived, detaching the chart nodes mid-assertion.

Also make the segment field list spec's database mock honour skip_fields, so
unit tests can catch a caller that stops asking for fields.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix be

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:22:06 -04:00
appleby
0a303f2dec Add Mistral metabot provider (#78712)
* BE Add Mistral metabot provider

* BE Add prompt-cache-key to LLMRequestOpts for Mistral prompt caching support

Mistral requires a `prompt_cache_key` to be set to enable prompt caching, so add it as an optional key to
`LLMRequestOpts` and thread it through `self/call-llm` and `self/call-llm-structured`.

* FE Add Mistral metabot provider

* Review feedback: fold all provider selectable tests into a single test

* Review feedback: use findPutRequests to reduce verbosity
2026-07-31 02:13:34 +00:00
appleby
75fb5a66ee Add Z.AI metabot provider (#78521)
* BE Add Z.AI metabot provider

* FE Add Z.AI metabot provider

* Move generic chat-completions code to metabase.metabot.self.openai.chat-completions

And use it in both the openrouter and zai providers.
2026-07-30 17:40:02 -07:00
Fraser
e491dcef5c Move search term hashing into the analytics module (#79124)
hashSearchTerm and shouldReportSearchTerm are analytics reporting
policy (settings read + SHA-256 hashing), not search UI, so they move
from common/search/term.ts to the lib-tier analytics module and are
exported from its barrel. Consumers now import them from
metabase/analytics, and the common/search barrel no longer re-exports
them.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 12:38:41 +12:00
Fraser
1e441d46c4 Re-tier routes-stable-id-aware from shared to app (#79116) 2026-07-31 12:38:25 +12:00
Fraser
0f1d14da69 Move funnel data types next to calculateFunnelSteps (#79122) 2026-07-31 12:38:13 +12:00
Fraser
ce12880118 Remove dead MetabotFooter component (#79114) 2026-07-31 12:37:57 +12:00
Fraser
2b0ed37c02 Move geojson endpoint into admin/settings (#79123) 2026-07-31 12:37:40 +12:00
Fraser
e61302efef Move question loader components into questions module (#79125) 2026-07-31 12:37:26 +12:00
Fraser
d7e7d184af Move isVisualizerDashboardCard into metabase-types/guards (#79120)
Relocate the isVisualizerDashboardCard type guard from
metabase/visualizer/utils to metabase-types/guards/dashboard, alongside
the other dashboard type guards, and update all importers to the new
path. Remove the re-export from the visualizer utils barrel and delete
the old file.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 12:37:12 +12:00
appleby
0ec7cdb8f9 Enable Mistral Medium 3.5 via OpenRouter (#78644) 2026-07-30 23:43:17 +00:00
Romeo Van Snick
8594eb45c7 Move the location.query readers to search params (#78670)
* Add parseSearchQuery and a useNavigationType facade hook

* Move the location.action readers to useNavigationType

* Move the location.query readers to search params

* Drop query and action from the facade Location
2026-07-31 09:07:55 +10:00
Romeo Van Snick
b4772edd33 Delete withRouteProps (#78779)
* Replace connect() with hooks in the app shells and permissions pages

* Rewrite the reference containers as function components

* Delete withRouteProps

* Pass the props reference children read but do not select

* Cover every reference container page with a render test

* Fetch reference container data before the child's first render
2026-07-31 09:07:36 +10:00
appleby
747194fddf Enable GLM-5.2 and Deepseek V4 Pro via OpenRouter (#78253) 2026-07-30 15:34:31 -07:00
Anderson
70a9109bf3 Track Dimensions tab views and dimension add/remove/reorder/edit events for metrics (#78721)
* Add Snowplow event tracking to the metric Dimensions tab

* Update events' definitions to comply with new approach

* Update analytics-events claude skill to account for new approach defined in #71631
2026-07-30 19:24:35 -03:00
Alexander Polyankin
a6a112c2ab Skip fields when browsing tables of a schema-less database (#79101)
TableBrowser only needs the table list, but GET /api/database/:id/metadata
returns every field of every table. Pass skip_fields=true so the response
stays proportional to the number of tables.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:27:53 -04:00
Anderson
06a0c8dcbc Allow setting custom time buckets for metric time dimensions (#79046)
* Allow setting custom time buckets for metric time dimensions

* Code cleanup

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>
2026-07-30 21:23:37 +00:00
Alexander Polyankin
860f2beff2 Make the permission check a hash lookup instead of a scan of every granted path (#79076) 2026-07-30 17:07:49 -04:00
Nick Fitzpatrick
3ceaa9583a 2FA Enrolment pages and reset (#78837)
* Enrolled / Unenrolled pages, routes

* Alessio Feedback

* settings card big title

* adjustments

* additional tests

* Kevin feedback

* braden feedback
2026-07-30 17:58:52 -03:00
Timothy S. Dean
959c2fca82 sql-jdbc: bind boolean insert values as parameters (#78864)
HoneySQL inlines true/false as TRUE/FLASE tokens even when the query is otherwise parameterized, and not every driver target has a boolean literal. SQL Server parses a bare TRUE as an identifier and `insert-into!` fails with `Invalid column name 'TRUE'`. Wrapping boolean row values in `[:lift v]` binds them as paremeters, which is what we do for other value types.

Impacts every `:sql-jdbc` driver's `insert-into!`. Drivers with boolean literals are not impacted.
2026-07-30 13:57:50 -06:00
Alexander Polyankin
60cbd1c85c Fix quadratic :is_personal hydration that made GET /api/collection ta… (#79073)
Fix quadratic :is_personal hydration that made GET /api/collection take ~23s

On an instance with 28k collections — 6k of them personal — `GET /api/collection`
took 23.5s while making only 13 DB calls. Nearly all of it was the `:is_personal`
batched hydration: for every collection it scanned the full set of personal
collection IDs, building a prefix string with `format` on each step, so a request
did ~20k x 6k = 125M `String/format` calls.

Personal Collections only ever live in the Root Collection, so a collection is
inside one exactly when the *first* ID of its location path is a personal
collection. Test that single ID against the set instead of scanning.

Measured on a seeded app DB matching the reported shape (28.6k collections, 6,011
personal roots, 13.6k live non-personal, 7.8k archived, depth to 13):

    GET /api/collection   11.2s -> 1.9s
    :is_personal hydrate   9.2s -> 75ms

The remaining hot spot is `:can_write` (~1.1-1.5s), which linearly scans the
user's permission set per collection — left for a separate change since it
touches the shared permission check.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:43:01 -04:00
Oleksandr Yakushev
714d6ceae1 [malli.defn] Don't gensym internal fn name twice (#78083) 2026-07-30 22:33:49 +03:00
Andrei Chernykh
af747a5b36 chore: Convert the store factory and whitelabel color bootstrap to TypeScript (#78618)
* Convert the store factory and whitelabel color bootstrap to TypeScript

* Name the deliberately loose reducer map type

* Condense the reducer map comment
2026-07-30 11:03:18 -07:00
da
ce9982a7e6 BigQuery: reject oversized queries client-side with a humanized error (#79020)
* BigQuery: reject oversized queries client-side with a humanized error

BigQuery rejects jobs whose SQL exceeds 1 MB (1,048,576 characters,
including comments and whitespace) with a raw 400 INVALID_ARGUMENT,
which Metabase surfaces to the user as "Error executing query: <raw
message>" -- accurate but technical and not actionable.

Check the final compiled SQL (including the appended remark) before the
BigQuery client/job is created and throw a localized, actionable
:invalid-query error instead. The raw DB error path remains as a
fallback if this limit ever drifts from BigQuery's own.

Quota reference: https://cloud.google.com/bigquery/quotas#query_limits

Closes #79019

* Update error message for SQL query length validation

* Address review: express limit as (* 1024 1024), slim the tests

- max-sql-query-length-chars: write the value as (* 1024 1024) so the
  1 MiB intent reads off the page.
- validate-query-length-test: collapse to the two boundary cases (at the
  limit allowed; over the limit throws :invalid-query), error type only,
  per review feedback to keep it simple.

* Address review: trim noise from max-sql-query-length-chars docstring

Drop the cross-reference to validate-query-length! and the "not ^:const"
justification — commentary, not contract. Leaves what the constant is,
how BigQuery counts, the failure mode, and the quota link.
2026-07-30 17:33:51 +00:00
Maksym Yakubych
ecf701f7f6 Add AI provider step to onboarding flow (#78001)
* Add AI provider setup to onboarding

- Insert AI config step into setup flow
- Track connect and skip analytics events
- Update onboarding tests and schema version

* Preserve existing AI provider during setup

- Wait for the saved provider before mounting the form
- Show the connected provider in the setup step and cover it in tests

* Defer AI provider sync until after setup

- Keep the AI config step visible during onboarding
- Skip legacy Metabot provider migration on fresh instances

* Add AI provider setup to onboarding

- Hide the step when AI features are disabled
- Support Anthropic onboarding setup in e2e
- Mock Anthropic model catalog for credential validation

* Match license step layout in AI setup step

* Fix modules config and flaky setup toast assertion

* Add free_units to managed AI e2e add-on fixture

* Condense AI step ordering comment

* Make AI setup opt-in from the completed step

The AI provider step is no longer part of the wizard by default. The
"You're all set up!" screen offers a "Set up AI" button that appends
ai_config as the last step; connecting or skipping returns to it.

* Scope the settings-list mock to the test that needs it

settingOverrides was only used by one embedding test, so drop it from the
shared setup helper and let that test override the settings-list route.
2026-07-30 17:15:21 +00:00
2958 changed files with 109018 additions and 37231 deletions

View File

@@ -27,9 +27,28 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
- Format using Markdown conventions
- Reference other vars with `[[other-var]]` not backticks
A docstring states the *contract*: what it returns (including when it returns nil), what its
params mean, what it throws versus swallows, and any precondition or invariant the caller must
respect. A params block for non-obvious or optional args is good documentation — length is not
the problem.
These seven things are, and each is a reason to cut or relocate:
| Anti-pattern | Why | Where it goes instead |
| --- | --- | --- |
| **Facts you don't own** — how a *callee*, another namespace, or another repo works internally | Drifts silently; nothing fails when it goes stale | Cut. Name the callee and let its own docstring say it |
| **Said twice** — ns docstring restating its vars', a caller restating its callee's, sibling fns repeating boilerplate | Two copies means one is already wrong | Cut the copy. One fact, one owner |
| **Design rationale** — why this approach, why it lives in this module, what the alternative was | The caller can't act on it | Commit message, or an inline comment on the specific line it constrains |
| **Development history** — "matching master", "the X branch", "currently", "for now", "used to" | Stale by construction | Cut |
| **Instructions to the reader** — "callers should trust this", "don't bother checking", "you should use X" | State the contract; the caller decides | Cut, keeping any actual guarantee as a plain statement |
| **Invented precision** — score bands, thresholds, or tables no code enforces | Reads as spec, isn't | Cut, or make the code enforce it |
| **A note on the wrong var** — describing how overrides behave, on the base/default impl | Nobody reading the override sees it | Move to the overrides it describes |
**Comments:**
- `TODO` format: `;; TODO (Name YYYY-MM-DD) -- description`
- Prefer an inline comment over docstring prose for anything that explains *the body*. Put it on
the line it explains, not at the top of the function.
## Code Organization

View File

@@ -6,3 +6,4 @@
- Run targeted tests, and lint continuously during development
- Prioritize understanding existing patterns before implementing
- Don't commit changes, leave it for the user to review and make commits
- Minimize comments. Document only non-obvious intent, trade-offs, or constraints that the code itself cannot convey. Do not narrate what the code does — keep code self-descriptive through naming and structure.

View File

@@ -10,23 +10,23 @@ This skill helps you add product analytics (Snowplow) events to track user inter
## Quick Reference
Analytics events in Metabase use Snowplow with typed event schemas. All events must be defined in TypeScript types before use.
Analytics events in Metabase use Snowplow with typed event schemas. Simple events are declared **where they are used** — `trackSimpleEvent` is generic and validates the payload at the call site.
**Key Files:**
- `frontend/src/metabase-types/analytics/event.ts` - Event type definitions
- `frontend/src/metabase-types/analytics/schema.ts` - Schema registry
- `frontend/src/metabase/analytics/` - Core tracking functions (import from `metabase/analytics`)
- Feature-specific `analytics.ts` files - Tracking function wrappers
- `frontend/src/metabase/analytics/event.ts` - Core tracking functions, `trackSimpleEvent` / `trackSchemaEvent` (import from `metabase/analytics`)
- `frontend/src/metabase-types/analytics/event.ts` - The shared `SimpleEventSchema` only. **Do not add event types here** (see below)
- `frontend/src/metabase-types/analytics/schema.ts` - Schema registry (custom/legacy schemas only)
- Feature-specific `analytics.ts` files - Where your tracking functions and any local types live
## Quick Checklist
When adding a new analytics event:
- [ ] Define event type in `frontend/src/metabase-types/analytics/event.ts`
- [ ] Add event to appropriate union type (e.g., `DataStudioEvent`, `SimpleEvent`)
- [ ] Create tracking function in feature's `analytics.ts` file
- [ ] Import and call tracking function at the interaction point
- [ ] Use `trackSimpleEvent()` for basic events (most common)
- [ ] Pick an event name (snake_case, past tense)
- [ ] Add a tracking function to the feature's `analytics.ts` file, calling `trackSimpleEvent()`
- [ ] Keep any field unions (e.g. `"success" | "failure"`) as local types in that same file
- [ ] Import and call the tracking function at the interaction point
- [ ] Do **not** add an event type to `metabase-types/analytics/event.ts` or to any union
## Event Schema Types
@@ -47,6 +47,25 @@ type SimpleEventSchema = {
**When to use:** 90% of events fit this schema. Use for clicks, opens, closes, creates, deletes, etc.
`trackSimpleEvent` is generic and enforces this schema on the object literal you pass it:
```typescript
// frontend/src/metabase/analytics/event.ts
export function trackSimpleEvent<
T extends SimpleEventSchema &
Record<Exclude<keyof T, keyof SimpleEventSchema>, never>,
>(event: T) {
trackSchemaEvent("simple_event", event);
}
```
That means a missing `event` or any field outside `SimpleEventSchema` is a compile error at the call
site. There is no separate event type to declare and no `satisfies` clause to add — the old
`ValidateEvent<...>` helper is no longer exported and is not part of the workflow.
`trackSchemaEvent` is generic too: it correlates the schema name with the payload type, so you can't
send a dashboard event under the `simple_event` schema.
### 2. Custom Schemas (legacy, no events are being added)
Consider adding new event schema only in very special cases.
@@ -57,36 +76,7 @@ Consider adding new event schema only in very special cases.
### Example: Track when a user applies filters in a table picker
#### Step 1: Define Event Types
Add event type definitions to `frontend/src/metabase-types/analytics/event.ts`:
```typescript
export type DataStudioTablePickerFiltersAppliedEvent = ValidateEvent<{
event: "data_studio_table_picker_filters_applied";
}>;
export type DataStudioTablePickerFiltersClearedEvent = ValidateEvent<{
event: "data_studio_table_picker_filters_cleared";
}>;
```
#### Step 2: Add to Union Type
Find or create the appropriate union type and add your events:
```typescript
export type DataStudioEvent =
| DataStudioLibraryCreatedEvent
| DataStudioTablePublishedEvent
| DataStudioGlossaryCreatedEvent
| DataStudioGlossaryEditedEvent
| DataStudioGlossaryDeletedEvent
| DataStudioTablePickerFiltersAppliedEvent // <- Add here
| DataStudioTablePickerFiltersClearedEvent; // <- Add here
```
#### Step 3: Create Tracking Functions
#### Step 1: Create Tracking Functions
In your feature's `analytics.ts` file (e.g., `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`):
@@ -106,7 +96,7 @@ export const trackDataStudioTablePickerFiltersCleared = () => {
};
```
#### Step 4: Use in Components
#### Step 2: Use in Components
Import and call the tracking function at the interaction point:
@@ -138,16 +128,11 @@ function FilterPopover({ filters, onSubmit }) {
## Using SimpleEventSchema Fields
All examples below live in the feature's own `analytics.ts` — nothing is registered centrally.
### Example: Event with target_id
```typescript
// Type definition
export type DataStudioLibraryCreatedEvent = ValidateEvent<{
event: "data_studio_library_created";
target_id: number | null;
}>;
// Tracking function
export const trackDataStudioLibraryCreated = (id: CollectionId) => {
trackSimpleEvent({
event: "data_studio_library_created",
@@ -162,14 +147,10 @@ trackDataStudioLibraryCreated(newLibrary.id);
### Example: Event with triggered_from
```typescript
// Type definition
export type NewButtonClickedEvent = ValidateEvent<{
event: "new_button_clicked";
triggered_from: "app-bar" | "empty-collection";
}>;
// Local union, exported only if another feature needs to pass the same value
export type NewButtonLocation = "app-bar" | "empty-collection";
// Tracking function
export const trackNewButtonClicked = (location: "app-bar" | "empty-collection") => {
export const trackNewButtonClicked = (location: NewButtonLocation) => {
trackSimpleEvent({
event: "new_button_clicked",
triggered_from: location,
@@ -187,44 +168,31 @@ export const trackNewButtonClicked = (location: "app-bar" | "empty-collection")
### Example: Event with event_detail
```typescript
// Type definition
export type MetadataEditEvent = ValidateEvent<{
event: "metadata_edited";
event_detail: "type_casting" | "semantic_type_change" | "visibility_change";
triggered_from: "admin" | "data_studio";
}>;
Real example — `frontend/src/metabase/metadata/pages/shared/analytics.ts`:
// Tracking function
export const trackMetadataChange = (
detail: "type_casting" | "semantic_type_change" | "visibility_change",
location: "admin" | "data_studio"
) => {
```typescript
export type MetadataEditEventDetail =
| "type_casting"
| "semantic_type_change"
| "visibility_change";
export const trackMetadataChange = (detail: MetadataEditEventDetail) => {
trackSimpleEvent({
event: "metadata_edited",
event_detail: detail,
triggered_from: location,
triggered_from: "admin",
});
};
// Usage
trackMetadataChange("semantic_type_change", "data_studio");
trackMetadataChange("semantic_type_change");
```
### Example: Event with result and duration
```typescript
// Type definition
export type MoveToTrashEvent = ValidateEvent<{
event: "moved-to-trash";
target_id: number | null;
triggered_from: "collection" | "detail_page" | "cleanup_modal";
duration_ms: number | null;
result: "success" | "failure";
event_detail: "question" | "model" | "metric" | "dashboard";
}>;
See `frontend/src/metabase/archive/analytics.ts` for the real version of this.
// Tracking function
```typescript
export const trackMoveToTrash = (params: {
targetId: number | null;
triggeredFrom: "collection" | "detail_page" | "cleanup_modal";
@@ -280,17 +248,16 @@ try {
"filters-applied" // Use underscore, not hyphen
```
### Event Type Names (PascalCase with "Event" suffix)
### Local Field Types (PascalCase, named after the field)
There is usually no `...Event` type to name anymore. When you do need a union for a field, name it
after the field it feeds:
```typescript
// Good
DataStudioLibraryCreatedEvent
TablePickerFiltersAppliedEvent
MetabotChatOpenedEvent
// Bad
dataStudioLibraryCreated // Wrong case
DataStudioLibraryCreated // Missing "Event" suffix
type MetricDimensionResult = "success" | "failure"; // -> result
export type MetadataEditEventDetail = "type_casting"; // -> event_detail
type NewButtonLocation = "app-bar" | "empty-collection"; // -> triggered_from
```
### Tracking Function Names (camelCase with "track" prefix)
@@ -309,29 +276,28 @@ logLibraryCreated // Use "track" prefix
## Common Patterns
### Pattern 1: Feature-Specific Union Types
### Pattern 1: Sharing Field Types Across Features
Group related events together:
When two features send the same event with a different `triggered_from`, export the field union from
the owning feature's `analytics.ts` and import it — don't hoist anything into `metabase-types`:
```typescript
export type DataStudioEvent =
| DataStudioLibraryCreatedEvent
| DataStudioTablePublishedEvent
| DataStudioGlossaryCreatedEvent;
// frontend/src/metabase/data-studio/data-model/analytics.ts
import { trackSimpleEvent } from "metabase/analytics";
import type { MetadataEditEventDetail } from "metabase/metadata/pages/shared/analytics";
export type MetabotEvent =
| MetabotChatOpenedEvent
| MetabotRequestSentEvent
| MetabotFixQueryClickedEvent;
// Then add to SimpleEvent union
export type SimpleEvent =
| /* other events */
| DataStudioEvent
| MetabotEvent
| /* more events */;
export function trackMetadataChange(detail: MetadataEditEventDetail) {
trackSimpleEvent({
event: "metadata_edited",
event_detail: detail,
triggered_from: "data_studio",
});
}
```
This is the point of the extensible-events design: enterprise and feature-tier types stay in their
own module instead of being imported down into a shared union.
### Pattern 2: Conditional Tracking
Track different events based on user action:
@@ -350,10 +316,10 @@ const handleSave = async () => {
## Common Pitfalls
### Don't: Add custom fields to SimpleEvent
### Don't: Add custom fields to a simple event
```typescript
// WRONG - SimpleEvent doesn't support custom fields
// WRONG - SimpleEventSchema doesn't support custom fields (this is a compile error)
export const trackFiltersApplied = (filters: FilterState) => {
trackSimpleEvent({
event: "filters_applied",
@@ -379,23 +345,43 @@ export const trackFilterApplied = (filterType: string) => {
};
```
### Don't: Forget to add event to union type
### Don't: Add event types to `metabase-types/analytics/event.ts`
The central `SimpleEvent` union was removed — it forced feature-tier types to be imported down into
shared code, causing module-boundary violations. `trackSimpleEvent` is generic now, so the type adds
nothing but duplication.
```typescript
// Define the event
// ❌ WRONG - central declaration + re-import for a `satisfies` clause
// frontend/src/metabase-types/analytics/event.ts
export type NewFeatureClickedEvent = ValidateEvent<{
event: "new_feature_clicked";
target_id: number;
}>;
// ❌ WRONG - Forgot to add to SimpleEvent union
// Event won't be recognized by TypeScript
// frontend/src/metabase/my-feature/analytics.ts
import type { NewFeatureClickedEvent } from "metabase-types/analytics";
// ✓ RIGHT - Add to appropriate union
export type SimpleEvent =
| /* other events */
| NewFeatureClickedEvent;
export const trackNewFeatureClicked = (id: number) => {
trackSimpleEvent({
event: "new_feature_clicked",
target_id: id,
} satisfies NewFeatureClickedEvent);
};
// ✓ RIGHT - the object literal is already checked by the generic
// frontend/src/metabase/my-feature/analytics.ts
export const trackNewFeatureClicked = (id: number) => {
trackSimpleEvent({
event: "new_feature_clicked",
target_id: id,
});
};
```
A few `...Event` types still sit in `metabase-types/analytics/event.ts`. They are leftovers from PRs
that landed around the refactor — don't copy them, and don't add to them.
### Don't: Mix up event name formats
```typescript
@@ -463,41 +449,44 @@ Example console output:
### Where to put tracking functions:
```
Feature-specific analytics functions:
Tracking functions AND their local field types (this is where new events live):
frontend/src/metabase/{feature}/analytics.ts
enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts
Event type definitions (all in one place):
frontend/src/metabase-types/analytics/event.ts
Core tracking utilities:
frontend/src/metabase/analytics/ (import from `metabase/analytics`)
Shared SimpleEventSchema only — nothing new goes here:
frontend/src/metabase-types/analytics/event.ts
```
In embedding SDK code, use `trackSdkSimpleEvent`
(`frontend/src/embedding-sdk-bundle/analytics/snowplow.ts`) instead — the main-app `"sp"` tracker
isn't initialized in the customer's page, so `trackSimpleEvent`'s Snowplow leg is a no-op there.
## Real-World Examples
See these files for reference:
- **Simple events**: `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`
- **Events with variants**: `frontend/src/metabase/dashboard/analytics.ts`
- **Complex events**: `frontend/src/metabase/query_builder/analytics.js`
- **Event type examples**: `frontend/src/metabase-types/analytics/event.ts`
- **Simple events + local field union**: `frontend/src/metabase/metadata/pages/shared/analytics.ts`
- **Reusing another feature's field type**: `frontend/src/metabase/data-studio/data-model/analytics.ts`
- **Result + duration timing**: `frontend/src/metabase/archive/analytics.ts`
- **Enterprise feature events**: `enterprise/frontend/src/metabase-enterprise/google_drive/analytics.ts`
## Workflow Summary
1. **Identify the user interaction** to track
2. **Decide on event name** (snake_case, descriptive)
3. **Define event type** in `event.ts` using `ValidateEvent`
4. **Add to union type** (create feature union if needed)
5. **Create tracking function** in feature's `analytics.ts`
6. **Import and call** at the interaction point
7. **Test** that events fire correctly
3. **Create tracking function** in feature's `analytics.ts`, calling `trackSimpleEvent()`
4. **Add local field unions** in that same file if a field has a fixed set of values
5. **Import and call** at the interaction point
6. **Test** that events fire correctly
## Tips
- **Be specific** - `filters_applied` is better than `action_performed`
- **Use past tense** - `library_created` not `create_library`
- **Group related events** - Create feature-specific event union types
- **Group related events** - Keep a feature's tracking functions together in its `analytics.ts`
- **Track meaningful actions** - Not every click needs tracking
- **Consider the data** - What would you want to analyze later?
- **Stay consistent** - Follow existing naming patterns in the codebase

View File

@@ -56,6 +56,11 @@ Use this to scan through changes efficiently:
- [ ] References use `[[other-var]]` not backticks
- [ ] `TODO` comments include author and date: `;; TODO (Name 2025-01-01) -- description`
Docstring content — check every new or rewritten docstring against the anti-pattern table in the
style guide above. Read the diff's docstrings on their own, separately from the code: prose is
where review attention slides off, and a long docstring reads as diligence rather than as the
liability it usually is.
### Code Organization
- [ ] Everything `^:private` unless used elsewhere
@@ -135,6 +140,17 @@ Quick scan for common issues:
> This public var needs a docstring explaining its purpose, inputs, and outputs.
**For docstrings documenting code they don't own:**
> This describes how `lib` decides binning strategies internally. Nothing here fails when that
> changes, so it will go stale silently — drop it and let `available-binning-strategies` answer
> for its own behavior.
**For docstring content that belongs in the body:**
> This paragraph explains why the branches are ordered this way, which is what someone editing
> the `cond` needs — but a caller can't act on it. Move it to an inline comment above the `cond`.
**For organization issues:**
> This function is only used in this namespace, so it should be marked `^:private`.

View File

@@ -82,10 +82,29 @@ either a bug, or it means callers must handle each case themselves — and
in *that* case it is the *absence* of the property that must be
documented. Document deviations from expectation, not conformance to it.
Heuristic: if a sentence would still be true after a full rewrite of the
body, it may belong in the docstring. If it describes *how the current
body works*, it belongs in the body — as an inline comment, if it is
non-obvious.
Heuristic — a sentence earns its place in the docstring only if it
passes both tests:
1. Rewrite test (necessary, not sufficient): if rewriting the body to a
different implementation with an identical contract could make the
sentence false, it describes the implementation, not the contract.
Relocate it to an inline comment at the point in the body where it
applies.
2. Ownership test: if the sentence states a fact about code this
function does not own — a callee's behavior, a library's guarantee —
it survives any rewrite trivially, which is exactly why the rewrite
test alone is not enough. If this body *relies* on that fact, it is
implementation context: relocate it to the call site. If nothing here
relies on it, it is blather about someone else's code: delete it, and
let the callee's own docstring answer for it.
Say each fact once. Duplicated prose is a stale comment in advance: when
the fact changes, one copy gets updated and the rest quietly rot. This
holds even within code you own — a namespace docstring that
re-summarizes its own multimethods, three sibling functions carrying the
same paragraph about retry behavior. Give the fact one home, and have
the other places name it rather than restate it.
Multi-line docstrings are not banned — a genuinely non-obvious constraint
the code had to deal with can be worth explaining. But be prudent; the

View File

@@ -7,7 +7,6 @@
:aliased-namespace-symbol {:level :warning}
:case-symbol-test {:level :warning}
:condition-always-true {:level :warning}
:def-fn {:level :warning}
:dynamic-var-not-earmuffed {:level :warning}
:equals-expected-position {:level :warning, :only-in-test-assertion true}
@@ -50,9 +49,11 @@
;; disabled linters
;;
:constant-condition {:level :off} ; TODO (sashko): too confused by defenterprises, some hook/config is needed
:redundant-ignore {:level :off} ; lots of false positives, see https://github.com/clj-kondo/clj-kondo/issues/2414
:unexpected-recur {:level :off} ; TODO (cam): I think we just need to tell it how to handle MBQL match and we can enable this?
:redundant-primitive-coercion {:level :off} ; there are many defensive boolean coercions, kondo looks too much into it
:type-mismatch {:level :off} ; Still quite raw, can sometimes give useful insights, but sometimes is just confused
;;
;; globally-enabled custom linters
@@ -503,7 +504,9 @@
metabase.query-processor.pipeline-queries-test/mbql5-query clojure.core/->
metabase.query-processor.pipeline-queries-test/run-mbql5-query clojure.core/->
metabase.query-processor.error-type/deferror clojure.core/def
metabase.query-processor.core/with-reducible-deserialized-results clojure.core/let
metabase.query-processor.middleware.cache.impl/with-reducible-deserialized-results clojure.core/let
metabase.query-processor.result-serialization/with-reducible-deserialized-results clojure.core/let
metabase.query-processor.middleware.process-userland-query-test/with-query-execution! clojure.core/let
metabase.query-processor.setup/with-qp-setup clojure.core/let
metabase.search.api-test/do-test-users clojure.core/let
@@ -520,7 +523,6 @@
metabase.test.data.interface/defdataset clojure.core/def
metabase.test.data.interface/defdataset-edn clojure.core/def
metabase.test.embedded-postgres.core/with-system clojure.core/let
metabase.test.gentest/defgentest clojure.test/deftest
metabase.test/defdataset clojure.core/def
metabase.test/let-url clojure.core/let
metabase.test/with-actions clojure.core/let
@@ -679,7 +681,6 @@
metabase.test.data/dataset hooks.metabase.test.data/dataset
metabase.test.data/mbql-query hooks.metabase.test.data/mbql-query
metabase.test.data/run-mbql-query hooks.metabase.test.data/mbql-query
metabase.test.gentest/iterate hooks.metabase.test.gentest/iterate*
metabase.test.util.async/with-open-channels hooks.common/let-with-optional-value-for-last-binding
metabase.test.util.generators.jvm/with-random-cards hooks.metabase.test.util.generators.jvm/with-random-cards
metabase.test.util.log/with-log-level hooks.common/with-ignored-first-arg
@@ -859,6 +860,11 @@
metabase.util.time-test
{:linters {:metabase/prefer-with-dynamic-fn-redefs {:level :off}}}
;; classloader sits below `util` in the module graph, so it can't use `metabase.util.log` without creating
;; a cycle; it gets to use tools.logging directly.
metabase.classloader.impl
{:linters {:discouraged-namespace {clojure.tools.logging {:level :off}}}}
source-namespaces
{:linters
{:metabase/discourage-millis-duration {:level :warning}

View File

@@ -302,6 +302,7 @@
:model/Database
:model/Dimension
:model/Document
:model/Exploration
:model/Measure
:model/ModelIndexValue
:model/NativeQuerySnippet
@@ -445,7 +446,8 @@
:model-imports #{:model/Card
:model/Collection
:model/Dashboard
:model/Document}}
:model/Document
:model/Exploration}}
bug-reporting
{:team "UX West"
@@ -539,7 +541,7 @@
{:team "DevEx"
:api #{metabase.classloader.core
metabase.classloader.init}
:uses #{util}}
:uses #{}}
cloud-migration
{:team "UX West"
@@ -610,6 +612,7 @@
:model/DashboardCard
:model/DashboardCardSeries
:model/Document
:model/Exploration
:model/NativeQuerySnippet
:model/Permissions
:model/Pulse
@@ -644,6 +647,7 @@
:model/Collection
:model/Dashboard
:model/Document
:model/Exploration
:model/NativeQuerySnippet
:model/Pulse
:model/Table
@@ -663,7 +667,7 @@
users
util}
:model-exports #{:model/Comment}
:model-imports #{:model/Document :model/User}}
:model-imports #{:model/Document :model/Exploration :model/User}}
;; technically this 'uses' `enterprise/core` and `test` since it tries to load them to see if they exists so we know
;; if EE/test code is available; however we can ignore them since they're not real usages.
@@ -695,6 +699,14 @@
:model-exports #{:model/ModerationReview}
:model-imports #{:model/Card :model/Dashboard}}
contextual-interestingness
{:team "UX West"
:api #{metabase.contextual-interestingness.core}
:uses #{interestingness
metabot
query-processor
util}}
core
{:team "UX West"
:api #{}
@@ -924,6 +936,7 @@
:model/DashboardTab
:model/Dimension
:model/Document
:model/Exploration
:model/Measure
:model/NativeQuerySnippet
:model/PermissionsGroup
@@ -998,6 +1011,47 @@
:model/Table
:model/User}}
explorations
{:team "UX West"
:api #{metabase.explorations.api
metabase.explorations.core
metabase.explorations.init}
:uses #{analytics
analytics-interface
api
app-db
collections
contextual-interestingness
events
interestingness
lib
lib-be
lib-metric
metrics
models
mq
permissions
queries
query-permissions
query-processor
request
search
settings
task
task-history
timeline
types
util}
:model-exports #{:model/Exploration}
:model-imports #{:model/Card
:model/Collection
:model/Comment
:model/Segment
:model/StoredResult
:model/StoredResultUse
:model/Timeline
:model/User}}
formatter
{:team "UX West"
:api #{metabase.formatter.core}
@@ -1152,7 +1206,6 @@
metabase.lib.schema.ref
metabase.lib.schema.template-tag
metabase.lib.schema.temporal-bucketing
metabase.lib.schema.test-spec
metabase.lib.schema.util
metabase.lib.schema.validate
metabase.lib.types.isa
@@ -1208,6 +1261,7 @@
metabase.llm.startup}
:uses #{analytics
api
config
driver
lib
lib-be
@@ -1216,6 +1270,7 @@
premium-features
request
settings
setup
sql-tools
sync
util
@@ -1322,8 +1377,8 @@
driver
entity-retrieval
events
explorations
interestingness
legacy-mbql
lib
lib-be
llm
@@ -1347,6 +1402,7 @@
sync
system
task
timeline
transforms
transforms-base
util
@@ -1378,7 +1434,6 @@
:api #{metabase.metrics.api
metabase.metrics.core}
:uses #{api
collections
events
lib
lib-be
@@ -1386,6 +1441,7 @@
models
parameters
permissions
queries
query-processor
request
server
@@ -1449,7 +1505,8 @@
mq
{:team "UX West"
:api #{metabase.mq.init}
:api #{metabase.mq.core
metabase.mq.init}
:uses #{analytics-interface
app-db
classloader
@@ -1734,6 +1791,7 @@
lib
models
notification
parameters
permissions
premium-features
query-processor
@@ -1800,7 +1858,9 @@
:model-exports #{:model/Card
:model/Query
:model/QueryExecution
:model/QueryTable}
:model/QueryTable
:model/StoredResult
:model/StoredResultUse}
:model-imports #{:model/Action
:model/CardBookmark
:model/Collection
@@ -1930,6 +1990,7 @@
:model-imports #{:model/Card
:model/DashboardCardSeries
:model/Database
:model/NativeQuerySnippet
:model/QueryCache
:model/QueryExecution
:model/Table}}
@@ -1939,6 +2000,7 @@
:api #{metabase.remote-sync.core
metabase.remote-sync.init}
:uses #{api
events
models
premium-features}
:model-imports #{:model/Collection}}
@@ -1982,6 +2044,7 @@
:model/DashboardCard
:model/DashboardTab
:model/Document
:model/Exploration
:model/Measure
:model/Segment
:model/Transform}}
@@ -2046,6 +2109,7 @@
:model-imports #{:model/Card
:model/Collection
:model/Dashboard
:model/Database
:model/Table
:model/User}}
@@ -2080,7 +2144,8 @@
metabase.server.middleware.session
metabase.server.settings
metabase.server.streaming-response} ; TODO -- too many API namespaces
:uses #{analytics
:uses #{agent-api
analytics
analytics-interface
api
api-keys
@@ -2229,7 +2294,8 @@
lib
settings
sql-parsing
util}}
util}
:model-imports #{:model/Table}}
sso
{:team "UX West"
@@ -2511,9 +2577,8 @@
typed-schemas
{:team "Embedding"
:api #{metabase.typed-schemas.api}
:api #{metabase.typed-schemas.core}
:uses #{actions
api
collections
lib
lib-be
@@ -2532,6 +2597,13 @@
:model/Measure
:model/Table}}
typed-schemas-rest
{:team "Embedding"
:api #{metabase.typed-schemas-rest.api}
:uses #{api
typed-schemas
util}}
types
{:team "Querying Platform"
:api #{metabase.types.core
@@ -2653,8 +2725,7 @@
:api :any
:uses #{classloader
config
settings
system}}
settings}}
version
{:team "UX West"
@@ -2826,6 +2897,7 @@
lib
lib-be
models
permissions
queries
query-permissions
query-processor
@@ -3071,6 +3143,7 @@
enterprise/custom-viz-plugin
{:team "Gadget"
:api #{metabase-enterprise.custom-viz-plugin.api
metabase-enterprise.custom-viz-plugin.api.sandbox-host
metabase-enterprise.custom-viz-plugin.init
metabase-enterprise.custom-viz-plugin.settings}
:uses #{api
@@ -3276,6 +3349,7 @@
:api #{metabase-enterprise.erd.api}
:uses #{api
models
permissions
util
warehouse-schema}
:model-imports #{:model/Database
@@ -3408,6 +3482,7 @@
config
events
premium-features
request
settings
sso
util}
@@ -3770,6 +3845,7 @@
:api #{metabase-enterprise.upload-management.api}
:uses #{api
models
permissions
premium-features
upload
util}

View File

@@ -5,35 +5,36 @@
;; to defend in your PR.
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
{:ignore-counts {:aliased-namespace-symbol 3
:all 50
:all 49
:case-symbol-test 1
:clojure-lsp/unused-public-var 9
:def-fn 1
:deprecated-namespace 95
:deprecated-var 81
:deprecated-var 79
:discouraged-java-method 3
:discouraged-namespace 80
:discouraged-var 106
:discouraged-namespace 79
:discouraged-var 107
:equals-true 1
:main-without-gen-class 1
:metabase/defsetting-namespace 7
:metabase/defsetting-namespace 2
: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
:metabase/modules 8
:metabase/modules 7
:metabase/namespace-name 4
:metabase/prefer-with-dynamic-fn-redefs 27
:metabase/prefer-with-dynamic-fn-redefs 28
:metabase/test-helpers-use-non-thread-safe-functions 17
:metabase/validate-defendpoint-has-response-schema 441
:metabase/validate-defendpoint-query-params-use-kebab-case 40
:metabase/validate-defendpoint-query-params-use-kebab-case 47
:metabase/validate-defendpoint-route-uses-kebab-case 44
:metabase/validate-deftest 8
:metabase/validate-deftest 9
:missing-docstring 10
:missing-protocol-method 3
:non-arg-vec-return-type-hint 1
:reduce-without-init 3
:redundant-fn-wrapper 1
:syntax 2
:uninitialized-var 1
:unquote-not-syntax-quoted 1
:unresolved-namespace 3
:unresolved-require 1

View File

@@ -1,14 +0,0 @@
(ns hooks.metabase.test.gentest
(:require
[clj-kondo.hooks-api :as hooks]))
(defn iterate*
"(iterate {:as limit-spec} [s1 val1 & more] body-form body-form...)"
[{{:keys [children] :as node} :node :as orig}]
(let [[token limit-spec bindings & body-nodes] children
node* (-> (hooks/list-node
(list token limit-spec
(hooks/list-node (list* (hooks/token-node 'let) bindings
body-nodes))))
(with-meta (meta node)))]
(assoc orig :node node*)))

13
.github/actionlint.yaml vendored Normal file
View File

@@ -0,0 +1,13 @@
self-hosted-runner:
# Custom runner labels that actionlint cannot know about. Everything else is
# selected through `vars.DEFAULT_RUNNER_KEY` and is not checked.
labels:
- arm-ubuntu-24-2core
paths:
# GitHub added parallel steps (`parallel`, `background`, `wait`, `wait-all`,
# `cancel`) in June 2026, and actionlint 1.7.12 does not know the schema yet:
# https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/
.github/workflows/lint.yml:
ignore:
- 'step must run script with "run" section or run action with "uses" section'

View File

@@ -1,4 +1,5 @@
name: Create Backport PR
description: Cherry-picks a commit onto a release branch and opens the backport pull request.
inputs:
target-version: # major version number like 34 or 86
@@ -42,7 +43,7 @@ runs:
git cherry-pick --abort
echo "Could not cherry pick because of a conflict"
echo "has-conflicts=true" >> $GITHUB_OUTPUT
echo "has-conflicts=true" >> "$GITHUB_OUTPUT"
# Add a shell script for resolving conflicts
echo "git reset HEAD~1" > ./backport.sh
@@ -66,7 +67,7 @@ runs:
END
)
else
echo "has-conflicts=false" >> $GITHUB_OUTPUT
echo "has-conflicts=false" >> "$GITHUB_OUTPUT"
PR_BODY="#${ORIGINAL_PULL_REQUEST_NUMBER}"
fi
@@ -80,7 +81,7 @@ runs:
BACKPORT_PR_URL=$(gh pr create "${PR_ARGS[@]}")
BACKPORT_PR_NUMBER=${BACKPORT_PR_URL##*/}
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> $GITHUB_OUTPUT
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "New PR has been created: #${BACKPORT_PR_NUMBER}"
git checkout ${{ github.sha }}

View File

@@ -1,4 +1,7 @@
name: Cross-branch migration test
description: >-
Boot Metabase on the base branch to migrate the app db, then boot the PR branch against the
same db, verifying the PR's migrations apply cleanly on top of the base branch's schema.
inputs:
base-ref: ## Base branch PR came from

View File

@@ -113,9 +113,9 @@ runs:
# Set the JAR_PATH environment variable
if [ "$EXTRACT_PATH" == "." ]; then
echo "JAR_PATH=$OUTPUT_NAME" >> $GITHUB_ENV
echo "JAR_PATH=$OUTPUT_NAME" >> "$GITHUB_ENV"
else
echo "JAR_PATH=$EXTRACT_PATH/$OUTPUT_NAME" >> $GITHUB_ENV
echo "JAR_PATH=$EXTRACT_PATH/$OUTPUT_NAME" >> "$GITHUB_ENV"
fi
- name: Verify that this is a valid JAR file

View File

@@ -22,7 +22,7 @@ runs:
COMMIT=$(env -i git log $BASE_REF --grep="(#$PULL_REQUEST_NUMBER)" --format="%H")
echo "commit SHA $COMMIT"
echo "commit=$COMMIT" >> $GITHUB_OUTPUT
echo "commit=$COMMIT" >> "$GITHUB_OUTPUT"
id: find-squashed-commit
shell: bash
env:

View File

@@ -36,10 +36,10 @@ runs:
WEEK_OF_YEAR=$(/bin/date "+%G-%V")
CACHE_SUFFIX="${{ runner.os }}-$WEEK_OF_YEAR"
echo "m2-cache-key=$M2_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "m2-restore-key=$M2_CACHE_PREFIX-" >> $GITHUB_OUTPUT
echo "node-cache-key=$NODE_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "node-restore-key=$NODE_CACHE_PREFIX-" >> $GITHUB_OUTPUT
echo "eslint-cache-key=$ESLINT_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "cypress-cache-key=$CYPRESS_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
echo "m2-cache-key=$M2_CACHE_PREFIX-$CACHE_SUFFIX" >> "$GITHUB_OUTPUT"
echo "m2-restore-key=$M2_CACHE_PREFIX-" >> "$GITHUB_OUTPUT"
echo "node-cache-key=$NODE_CACHE_PREFIX-$CACHE_SUFFIX" >> "$GITHUB_OUTPUT"
echo "node-restore-key=$NODE_CACHE_PREFIX-" >> "$GITHUB_OUTPUT"
echo "eslint-cache-key=$ESLINT_CACHE_PREFIX-$CACHE_SUFFIX" >> "$GITHUB_OUTPUT"
echo "cypress-cache-key=$CYPRESS_CACHE_PREFIX-$CACHE_SUFFIX" >> "$GITHUB_OUTPUT"
shell: bash

View File

@@ -35,7 +35,7 @@ runs:
run: |
BUN_CACHE_DIR=$(bun pm cache)
echo "Bun cache directory: $BUN_CACHE_DIR"
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
echo "dir=$BUN_CACHE_DIR" >> "$GITHUB_OUTPUT"
shell: bash
- name: Restore node_modules cache
@@ -53,7 +53,13 @@ runs:
# If a job wants to patch packages, it does so explicitly in the CI. This setting is opt-out.
# For example, we never want to cache node_modules with patches already applied so cache-generator.yml skips that step.
- name: Clean install
run: bun install --frozen-lockfile
# Remove the restored node_modules before installing. The os-week cache can
# carry a stale tree (for example a different React major), and
# `--frozen-lockfile` alone does not prune it. Rebuilding from the cached bun
# store gives an exact, lockfile-correct node_modules with no extra download.
run: |
rm -rf node_modules
bun install --frozen-lockfile
shell: bash
- name: Patch frontend dependencies
if: inputs.apply-patches == 'true'

View File

@@ -88,9 +88,9 @@ runs:
REPO_URL="https://github.com/metabase/shoppy.git"
echo "Checking if branch '${SAMPLE_APP_BRANCH_NAME}' exists in ${REPO_URL}..."
if git ls-remote --exit-code --heads "$REPO_URL" "${SAMPLE_APP_BRANCH_NAME}"; then
echo "sample_app_branch_exists=true" >> $GITHUB_OUTPUT
echo "sample_app_branch_exists=true" >> "$GITHUB_OUTPUT"
else
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
echo "sample_app_branch_exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Configure AWS credentials

View File

@@ -1,26 +0,0 @@
name: Prepare Uberjar Artifact
description: Prepare and upload Metabase JAR as a GitHub artifact.
inputs:
edition:
required: false
default: "ee"
description: Metabase edition (ee or oss)
runs:
using: "composite"
steps:
- name: Mark with the commit hash
run: git rev-parse --short HEAD > COMMIT-ID
shell: bash
- name: Calculate SHA256 checksum
run: sha256sum ./target/uberjar/metabase.jar > SHA256.sum
shell: bash
- name: Upload JARs as artifact
uses: actions/upload-artifact@v7
with:
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
path: |
./target/uberjar/metabase.jar
./COMMIT-ID
./SHA256.sum

View File

@@ -9,20 +9,26 @@ inputs:
build-static-viz:
required: true
default: false
logs-artifact-suffix:
artifact-suffix:
required: false
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
description: >-
Discriminator appended to this job's artifact names (test logs, granular-rerun failed
tests). A job whose matrix runs several concurrent legs MUST set a per-leg-unique value,
or the legs collide on one artifact — for the rerun artifact that means a leg restores
another leg's failed set. Must be stable across attempts of the same job.
edition:
required: false
default: "ee"
description: Edition to test (oss or ee)
quarantine-engine:
required: false
driver-status:
granular-rerun:
required: false
default: ""
default: "true"
description: >-
Configured status for this driver ("info", "skip", "required", or empty), as reported by `mage driver-decisions`.
When "true" (default), a re-run of a failed job runs only the tests that failed on the
previous attempt (hawk `:only`). Stress workflows set this to "false": they intentionally
run a fixed test set `:times N`, and narrowing to failures would drop that repetition.
outputs:
test-outcome:
@@ -48,12 +54,63 @@ runs:
env:
MB_EDITION: ${{ inputs.edition }}
# Granular rerun: on a re-run of a failed job, restore the list
# of tests that failed on the previous attempt (uploaded below) so the test
# step can target only those instead of the whole suite. Best-effort: a
# missing/empty artifact leaves no file and the run falls back to the full
# suite. First attempts skip this entirely.
#
# Scoped to the IMMEDIATELY preceding attempt: an attempt that passed, or
# that failed with nothing attributable to a var, uploads no artifact, and a
# narrow set must not outlive it. Reading `attempt - 1` means "only what the
# last attempt actually recorded" — anything else falls back to the full suite.
# Workflow expressions have no arithmetic, so the shell computes it.
#
# run-tests.yml calls app-db.yml, backend.yml, drivers.yml and semantic-search.yml
# as part of ONE run, so all four share an artifact namespace. `junit-name` alone is not
# unique there — app-db.yml and drivers.yml both derive `be-tests-postgres-ee` — so
# `artifact-suffix` carries the rest of the discriminator.
- name: Resolve rerun artifact names
if: ${{ inputs.granular-rerun == 'true' }}
shell: bash
env:
RERUN_ARTIFACT_BASE: ${{ format('failed-tests-{0}{1}', inputs.junit-name, inputs.artifact-suffix && format('-{0}', inputs.artifact-suffix) || '') }}
run: |
echo "RERUN_ARTIFACT=$RERUN_ARTIFACT_BASE-attempt-$GITHUB_RUN_ATTEMPT" >> "$GITHUB_ENV"
echo "RERUN_PREV_ARTIFACT=$RERUN_ARTIFACT_BASE-attempt-$((GITHUB_RUN_ATTEMPT - 1))" >> "$GITHUB_ENV"
# `pattern` (not `name`) because the artifact is legitimately absent whenever the
# previous attempt recorded nothing: `name` treats that as an error, `pattern`
# downloads nothing and moves on. The value is a literal — no glob metacharacters —
# so it matches exactly one artifact, which lands at `target/failed-tests`. A pattern
# that ever matched two would nest them under their artifact names instead, leaving
# the file absent and falling back to the full suite.
- name: Restore previously-failed tests
if: ${{ inputs.granular-rerun == 'true' && github.run_attempt != '1' }}
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: ${{ env.RERUN_PREV_ARTIFACT }}
path: target
- name: Test database driver
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
# On a rerun with a non-empty failed-tests file, target only those vars via
# hawk `:only` (a var-level selector bypasses `:only-tags`, which is fine —
# every listed var already passed that filter on the first attempt).
# Otherwise run exactly as before. Quote the file so the whole `[ns/var ...]`
# vector reaches clojure -X as a single argument.
run: |
BASE="clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test"
if [ "${{ 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)"
else
$BASE ${{ inputs.test-args }}
fi
shell: bash
id: run-test
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
# Driver "info" status can affect this logic. See below.
# Keep this step continue-on-error because the quarantine gate below controls whether it
# ultimately fails.
continue-on-error: true
- name: Report failed tests to ci-conductor
@@ -65,6 +122,30 @@ runs:
test-suite: ${{ inputs.junit-name }}
adapter: backend
# Granular rerun: record which tests failed so a re-run of this
# job targets only those (restored above). Reuses the backend JUnit adapter
# from the ci-conductor sources the report step just prepared. Best-effort:
# writes nothing unless every failure is attributable to a var, so an
# unattributable failure falls back to a full rerun.
- name: Collect failed tests for rerun
if: ${{ inputs.granular-rerun == 'true' && !cancelled() && steps.run-test.outcome == 'failure' }}
continue-on-error: true
shell: bash
# Clear the restored set first, so a collector that dies before deciding
# can't leave the previous attempt's file to be uploaded as this one's.
run: |
rm -f target/failed-tests
bun release/ci-conductor/src/collect-failed-backend.ts
- name: Upload failed tests for rerun
if: ${{ inputs.granular-rerun == 'true' && !cancelled() && steps.run-test.outcome == 'failure' }}
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: ${{ env.RERUN_ARTIFACT }}
path: target/failed-tests
if-no-files-found: ignore
- name: Publish Test Report (JUnit)
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
# As per the documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#steps-context
@@ -83,7 +164,7 @@ runs:
if: always() && steps.run-test.outcome != 'success'
uses: actions/upload-artifact@v7
with:
name: ${{ format('test-logs-{0}{1}', github.job, inputs.logs-artifact-suffix && format('-{0}', inputs.logs-artifact-suffix) || '') }}
name: ${{ format('test-logs-{0}{1}', github.job, inputs.artifact-suffix && format('-{0}', inputs.artifact-suffix) || '') }}
path: logs/test-log.json
retention-days: 1
# Failing to upload the artifact should never affect the outcome of the whole test run.
@@ -92,13 +173,7 @@ runs:
# Last, because this is the only step allowed to fail the job: everything above it reports the
# failure (JUnit report, logs, ci-conductor) and must run regardless of the verdict below.
- name: Quarantine gate
# Off master and release branches an "info" driver is data-collection only - the test always passes!
# On master and release branches "info" carries no weight - the quarantine logic applies.
if: |
!cancelled()
&& steps.run-test.outcome == 'failure'
&& !(inputs.driver-status == 'info'
&& !(github.ref_name == 'master' || startsWith(github.ref_name, 'release-x')))
if: ${{ !cancelled() && steps.run-test.outcome == 'failure' }}
uses: ./.github/actions/quarantine-gate
with:
adapter: backend

View File

@@ -1,4 +1,5 @@
name: Create Backend Dep Bump PRs
description: Opens a pull request for each backend dependency that has a newer version available.
inputs:
github-token:

View File

@@ -1,185 +0,0 @@
name: Upload Test Results to s3
description: Uploads test results files to s3
inputs:
input-path:
required: true
output-name:
required: true
default: test
role-to-assume:
required: true
aws-region:
required: true
default: us-east-1
bucket:
required: true
trunk-api-token:
required: true
variant:
required: false
default: ""
previous-step-outcome:
required: true
driver-status:
required: false
default: ""
description: >-
Configured status for this driver ("info", "skip", "required", or empty). When
"info", the job is reported as passing regardless of the real test outcome, and
that outcome is surfaced via a job summary and (on PRs) a comment instead.
report-variant:
required: false
default: ""
description: >-
Optional label disambiguating matrix legs of the same job, so the info-driver
job summary and PR comment stay distinct per leg.
run-trunk:
required: false
default: "true"
description: Whether to run the Trunk step at all.
runs:
using: "composite"
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885
with:
role-to-assume: ${{ inputs.role-to-assume }}
aws-region: ${{ inputs.aws-region }}
- name: zip test results
env:
INPUT_DIR: ${{ inputs.input-path }}
OUTPUT_FILE: ${{ inputs.output-name }}
shell: bash
run: | # sh
zip ${OUTPUT_FILE}.zip $INPUT_DIR/*.xml
- name: Upload test results to S3
env:
BUCKET: ${{ inputs.bucket }}
OUTPUT_FILE: ${{ inputs.output-name }}
shell: bash
run: | # sh
DATE=$(date '+%Y-%m-%d')
aws s3 cp ${OUTPUT_FILE}.zip s3://$BUCKET/$DATE/$GITHUB_RUN_ID/$GITHUB_RUN_ATTEMPT/
- name: Upload results to Trunk
if: ${{ always() && github.repository_owner == 'metabase' && inputs.run-trunk == 'true' }}
# Important!
# For "info" drivers off master/release branches, our own status takes precedence: we let this
# step pass so the job is reported as passing regardless of the real outcome (still uploaded).
# On master and release branches, info status is ignored and the real outcome gates the job.
continue-on-error: ${{ inputs.driver-status == 'info' && !(github.ref_name == 'master' || startsWith(github.ref_name, 'release-x')) }}
uses: trunk-io/analytics-uploader@1198efcc0976501147b10a7e343c3fa069527526 # v2.0.9
env:
TRUNK_REPO_URL: git@github.com:metabase/metabase.git
with:
junit-paths: ${{ inputs.input-path }}
variant: ${{ inputs.variant || inputs.output-name }}
org-slug: metabase
dry-run: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == true) || github.repository != 'metabase/metabase' }} # quarantine, but do not upload results
token: ${{ inputs.trunk-api-token }}
use-cache: true
previous-step-outcome: ${{ inputs.previous-step-outcome }}
# For "info" drivers the job is forced green above, so surface the real outcome here —
# but only when it did NOT pass, to avoid spamming summaries/comments for healthy runs.
# Never include logs or stack traces — only the real status and a link to the run.
- name: Report info-level driver result (job summary)
if: ${{ always() && inputs.driver-status == 'info' && inputs.previous-step-outcome == 'failure' }}
shell: bash
env:
OUTCOME: ${{ inputs.previous-step-outcome }}
LABEL: ${{ inputs.output-name }}
VARIANT: ${{ inputs.report-variant }}
run: | # sh
if [ "$OUTCOME" = "success" ]; then EMOJI="✅"; else EMOJI="❌"; fi
TITLE="$LABEL"
[ -n "$VARIANT" ] && TITLE="$LABEL ($VARIANT)"
RUN_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
{
echo "### ℹ️ Info-level driver: $TITLE"
echo ""
echo "Reported as passing (non-blocking). Real result: $EMOJI \`$OUTCOME\`"
echo ""
echo "[View run]($RUN_URL)"
} >> "$GITHUB_STEP_SUMMARY"
# Maintain ONE PR comment per commit for all info drivers. Each leg upserts its own line
# (keyed by a hidden tag); a passing leg removes its line, and the comment is deleted once
# no failing legs remain. Comments from older commits are cleaned up. Runs on pass and fail
# so recoveries self-heal. Never includes logs or stack traces — only status and a run link.
- name: Sync info-level driver result into the PR comment
if: ${{ always() && inputs.driver-status == 'info' && github.event_name == 'pull_request' }}
uses: actions/github-script@v9
continue-on-error: true
env:
OUTCOME: ${{ inputs.previous-step-outcome }}
LABEL: ${{ inputs.output-name }}
VARIANT: ${{ inputs.report-variant }}
with:
script: | # js
const { OUTCOME, LABEL, VARIANT, GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID } = process.env;
const pr = context.payload.pull_request;
if (!pr) return;
const { owner, repo } = context.repo;
const sha = pr.head.sha;
// PREFIX matches the current per-commit marker AND the older per-leg one, so legacy
// comments from a previous version get cleaned up on the next sync.
const PREFIX = "<!-- info-driver-result";
const marker = `<!-- info-driver-results:${sha} -->`;
const header = `${marker}\nℹ️ **Info-level drivers** failed (non-blocking) on \`${sha.slice(0, 7)}\` — these do not gate:`;
const lineKey = `${LABEL}:${VARIANT}`;
const title = VARIANT ? `${LABEL} (${VARIANT})` : LABEL;
const runUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;
// Only real failures are reported; cancelled/skipped/success remove this leg's line.
// List-item line with the hidden key tag at the END so markdown (links) renders.
const ourLine = OUTCOME === "failure"
? `- **${title}** — ❌ \`${OUTCOME}\` ([Logs](${runUrl})) <!--l:${lineKey}-->`
: null;
const linesOf = (body) => body.split("\n").filter((l) => l.includes("<!--l:"));
const sync = async () => {
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: pr.number, per_page: 100,
});
const tagged = comments.filter((c) => c.body.includes(PREFIX));
// Drop comments left over from older commits.
for (const c of tagged.filter((c) => !c.body.includes(marker))) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }).catch(() => {});
}
const mine = tagged
.filter((c) => c.body.includes(marker))
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
// Merge any duplicates raced into existence; latest write of each line wins.
const byKey = new Map();
for (const c of mine) {
for (const l of linesOf(c.body)) {
const m = l.match(/<!--l:(.*?)-->/);
if (m) byKey.set(m[1], l);
}
}
if (ourLine) byKey.set(lineKey, ourLine);
else byKey.delete(lineKey);
const lines = [...byKey.values()].sort();
const target = mine[0];
for (const c of mine.slice(1)) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }).catch(() => {});
}
if (lines.length === 0) {
if (target) await github.rest.issues.deleteComment({ owner, repo, comment_id: target.id }).catch(() => {});
return;
}
const body = [header, "", ...lines].join("\n");
if (target) {
await github.rest.issues.updateComment({ owner, repo, comment_id: target.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}
};
// Retry to shrink the window where parallel legs clobber each other's update.
for (let i = 0; i < 4; i++) {
try { await sync(); break; }
catch (e) { if (i === 3) throw e; await new Promise((r) => setTimeout(r, 400 * (i + 1))); }
}

View File

@@ -1,22 +0,0 @@
name: 'Verify Organization Membership'
description: 'Check if a user is a member of a GitHub organization'
inputs:
github_token:
description: 'Token with access to the organization'
required: true
organization_name:
description: 'Organization name'
required: true
username:
description: 'Username'
required: true
outputs:
is_member:
value: ${{ steps.verify-membership.outputs.is_member }}
description: "Is the user a member of the organization"
runs:
using: "composite"
steps:
- run: bash ${{ github.action_path }}/verify_membership.sh ${{ inputs.github_token }} ${{ inputs.organization_name }} ${{ inputs.username }}
id: verify-membership
shell: bash

View File

@@ -1,25 +0,0 @@
#!/bin/bash
TOKEN=$1
ORG_NAME=$2
USERNAME=$3
response=$(curl -L -o /dev/null -w "%{http_code}" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG_NAME/members/$USERNAME")
# Github returns an empty response with 204 status if user is a member and 404 status otherwise
# See: https://docs.github.com/en/rest/orgs/members?apiVersion=2022-11-28#check-organization-membership-for-a-user
if [ "$response" == "204" ]; then
echo "The user $USERNAME is a member of $ORG_NAME"
echo "is_member=true" >> $GITHUB_OUTPUT
elif [ "$response" == "404" ]; then
echo "The user $USERNAME is not a member of $ORG_NAME"
echo "is_member=false" >> $GITHUB_OUTPUT
else
echo "Failed to determine the membership status for $USERNAME in $ORG_NAME"
echo "is_member=false" >> $GITHUB_OUTPUT
exit 1
fi

View File

@@ -106,13 +106,6 @@ backend_ci: &backend_ci
- ".github/workflows/backend.yml"
- ".github/workflows/drivers.yml"
backend_presto_kerberos:
- "**/presto_jdbc/**"
- "**/presto-jdbc/**" # the driver module has a hyphen. underscores are classpaths
- "**/presto_jdbc.clj"
- "**/presto_jdbc_test.clj"
- ".github/workflows/presto-kerberos-integration-test.yml"
backend_sources: &backend_sources
- *shared_sources
- "**deps.edn"
@@ -150,7 +143,7 @@ custom_viz_all:
e2e_ci: &e2e_ci
- *shared_ci
- ".github/actions/prepare-uberjar-artifact/**"
- ".github/workflows/uberjar.yml"
- ".github/actions/fetch-artifact/action.yml"
- ".github/actions/e2e-prepare-containers/**"
- ".github/actions/prepare-cypress/**"
@@ -161,7 +154,7 @@ e2e_specs: &e2e_specs
embedding_sdk_ci: &embedding_sdk_ci
- *shared_ci
- ".github/actions/prepare-uberjar-artifact/**"
- ".github/workflows/uberjar.yml"
- ".github/actions/fetch-artifact/action.yml"
- ".github/actions/prepare-cypress/**"
- ".github/workflows/embedding-sdk*.yml"
@@ -183,7 +176,7 @@ embedding_sdk_host_sample_apps:
- "e2e/test-host-app/**/*.cy.*.{js,ts}"
snowplow: &snowplow
- ".github/workflows/snowplow.yml"
- ".github/workflows/lint.yml"
- "snowplow/**"
- "bin/lint-snowplow-schemas.bb"
- "bin/lint-snowplow-schemas-test.bb"
@@ -212,7 +205,7 @@ yaml:
- "**/*.yaml"
codeql:
- ".github/workflows/codeql.yml"
- ".github/workflows/lint.yml"
- "frontend/src/**"
- "enterprise/frontend/src/**"
@@ -220,14 +213,9 @@ i18n:
- *default
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/workflows/i18n.yml"
- ".github/workflows/lint.yml"
- *sources
visualizations:
- "frontend/src/metabase/visualizations/**"
- "frontend/src/metabase/static-viz/**"
- "src/metabase/pulse/**"
cljs:
- *shared_sources
- *shared_specs
@@ -239,7 +227,7 @@ serialization_baseline:
- "test_resources/serialization_baseline/**"
openapi:
- ".github/workflows/openapi.yml"
- ".github/workflows/lint.yml"
- "src/metabase/api/**"
- "src/metabase/api_routes/**"
- "src/metabase/*_rest/api.clj"

View File

@@ -1,7 +1,12 @@
import micromatch from "micromatch";
export type ModuleDef = { type: string; pattern: string };
export type Rule = { from: string[]; allow: string[] };
export type Rule = {
from: string[];
allow?: string[];
disallow?: string[];
};
export type ModuleNode = { type: string; regex: RegExp };
@@ -203,8 +208,11 @@ export function buildModuleGraph(
allTypes.map((type) => [type, new Set()]),
);
for (const rule of rules) {
// Grandfathered imports that break the disallow rules still exist in the code,
// and selection must cover every import that actually exists.
// So disallow rules are ignored, at the cost of a looser graph.
const fromTypes = rule.from.flatMap(expandPattern);
const allowTypes = rule.allow.flatMap(expandPattern);
const allowTypes = (rule.allow ?? []).flatMap(expandPattern);
for (const target of allowTypes) {
for (const importer of fromTypes) {
if (importer !== target) {

View File

@@ -0,0 +1,66 @@
/**
* Renames aliased dependency keys in a bun-generated yarn.lock.
*
* Yarn names an alias entry after the alias, for example
* `"typescript7@npm:typescript@7.0.2"`. Bun names it after the aliased package
* instead, for example `"typescript@npm:typescript@7.0.2"`. Snyk follows the
* yarn convention and fails to resolve the dependency.
*/
import { readFile, writeFile } from "node:fs/promises";
async function main() {
const packageJsonPath = `${process.cwd()}/package.json`;
const lockPath = `${process.cwd()}/yarn.lock`;
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
const dependencies = Object.entries<string>(
Object.assign(
{},
packageJson.dependencies,
packageJson.devDependencies,
packageJson.optionalDependencies,
),
);
let lock = await readFile(lockPath, "utf8");
const renamedKeys: string[] = [];
for (const [alias, specifier] of dependencies) {
if (!specifier.startsWith("npm:")) {
continue;
}
const aliasedPackage = specifier
.slice("npm:".length)
.replace(/@[^@]*$/, "");
if (aliasedPackage === alias) {
continue;
}
const bunKey = `"${aliasedPackage}@${specifier}"`;
const yarnKey = `"${alias}@${specifier}"`;
if (lock.includes(bunKey)) {
lock = lock.replaceAll(bunKey, yarnKey);
renamedKeys.push(`${bunKey} -> ${yarnKey}`);
}
}
if (renamedKeys.length > 0) {
await writeFile(lockPath, lock);
}
process.stdout.write(
renamedKeys.length > 0
? `Renamed alias keys in yarn.lock:\n${renamedKeys.join("\n")}\n`
: "No alias keys to rename in yarn.lock\n",
);
}
main().catch((error) => {
process.stderr.write(`Failed to rename alias keys in yarn.lock: ${error}\n`);
process.exit(1);
});

View File

@@ -1,84 +0,0 @@
const githubSlackMap = require("../../release/github-slack-map.json");
async function getIssueData({ context, github, manualIssueNumber }) {
let issueNumber;
let issueData;
let prReviews = [];
const triggerType = context.eventName;
if (context.eventName === "issues") {
issueNumber = context.payload.issue.number;
issueData = context.payload.issue;
} else if (context.eventName === "workflow_dispatch") {
issueNumber = parseInt(manualIssueNumber);
const { data } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
issueData = data;
} else if (context.eventName === "pull_request_review") {
const prNumber = context.payload.pull_request.number;
const { data: prData } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
// Extract issue number from branch name (e.g., "claude-fix/issue-12345-some-title")
const branchMatch = prData.head.ref.match(/claude-fix\/issue-(\d+)/);
if (!branchMatch) {
throw new Error(
`Could not extract issue number from branch name: ${prData.head.ref}`,
);
}
issueNumber = parseInt(branchMatch[1]);
const { data } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
issueData = data;
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
prReviews = reviews.filter(
(review) =>
review.state === "CHANGES_REQUESTED" &&
isMetabaseMember(review.user.login),
);
}
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
const employeeComments = comments.filter((comment) =>
isMetabaseMember(comment.user.login),
);
return {
issueNumber,
issueData,
employeeComments,
prReviews,
triggerType,
isIssueAuthorEmployee: isMetabaseMember(issueData.user.login),
};
}
function isMetabaseMember(username) {
return username in githubSlackMap;
}
module.exports = {
getIssueData,
};

View File

@@ -1,65 +0,0 @@
#! /usr/bin/env bash
# runs one or more Metabase test(s) against a Kerberized Presto instance
set -eo pipefail
# Need Java commands on $PATH, which apparently is not yet the case
export JAVA_HOME="/usr/lib/jvm/msopenjdk-current"
export PATH="$PATH:$JAVA_HOME/bin"
# ensure java commmand is available
which java
# install clojure version needed for Metabase
curl -O https://download.clojure.org/install/linux-install-1.12.0.1488.sh
chmod +x linux-install-1.12.0.1488.sh
./linux-install-1.12.0.1488.sh
RESOURCES_DIR=/app/source/resources
# ensure the expected files are in place, in the resources dir
if [ ! -f "$RESOURCES_DIR/ssl_keystore.jks" ]; then
echo "$RESOURCES_DIR/ssl_keystore.jks does not exist; cannot run test" >&2
exit 11
fi
if [ ! -f "$RESOURCES_DIR/krb5.conf" ]; then
echo "$RESOURCES_DIR/krb5.conf does not exist; cannot run test" >&2
exit 12
fi
if [ ! -f "$RESOURCES_DIR/client.keytab" ]; then
echo "$RESOURCES_DIR/client.keytab does not exist; cannot run test" >&2
exit 13
fi
# Copy the JDK cacerts file to our resources
cp $JAVA_HOME/lib/security/cacerts $RESOURCES_DIR/cacerts-with-presto-ca.jks
# Capture the Presto server self signed CA in PEM format
openssl s_client -showcerts -connect presto-kerberos:7778 </dev/null \
| openssl x509 -outform PEM >$RESOURCES_DIR/presto-ssl-root-ca.pem
# Convert the Presto server self signed CA to DER format
openssl x509 -outform der -in $RESOURCES_DIR/presto-ssl-root-ca.pem -out $RESOURCES_DIR/presto-ssl-root-ca.der
# Add Presto's self signed CA to the truststore
keytool -noprompt -import -alias presto-kerberos -keystore $RESOURCES_DIR/cacerts-with-presto-ca.jks \
-storepass changeit -file $RESOURCES_DIR/presto-ssl-root-ca.der -trustcacerts
# Set up the environment variables pointing to all of this, and run some tests
DRIVERS=presto-jdbc \
MB_ENABLE_PRESTO_JDBC_DRIVER=true \
MB_PRESTO_JDBC_TEST_HOST=presto-kerberos \
MB_PRESTO_JDBC_TEST_PORT=7778 \
MB_PRESTO_JDBC_TEST_SSL=true \
MB_PRESTO_JDBC_TEST_SSL_KEYSTORE_PATH=$RESOURCES_DIR/ssl_keystore.jks \
MB_PRESTO_JDBC_TEST_SSL_KEYSTORE_PASSWORD=presto \
MB_PRESTO_JDBC_TEST_SSL_TRUSTSTORE_PATH=$RESOURCES_DIR/cacerts-with-presto-ca.jks \
MB_PRESTO_JDBC_TEST_SSL_TRUSTSTORE_PASSWORD=changeit \
MB_PRESTO_JDBC_TEST_KERBEROS=true \
MB_PRESTO_JDBC_TEST_USER=bob@EXAMPLE.COM \
MB_PRESTO_JDBC_TEST_KERBEROS_PRINCIPAL=bob@EXAMPLE.COM \
MB_PRESTO_JDBC_TEST_KERBEROS_REMOTE_SERVICE_NAME=HTTP \
MB_PRESTO_JDBC_TEST_KERBEROS_KEYTAB_PATH=$RESOURCES_DIR/client.keytab \
MB_PRESTO_JDBC_TEST_KERBEROS_CONFIG_PATH=$RESOURCES_DIR/krb5.conf \
clojure -X:dev:test:drivers:drivers-dev :only metabase.driver.presto-jdbc-test

4
.github/team.json vendored
View File

@@ -64,7 +64,9 @@
"lbrdnk",
"piranha",
"crisptrutski",
"undrfined"
"undrfined",
"andreis",
"prascuna"
]
},
{

View File

@@ -1,44 +0,0 @@
name: Actionlint
on:
push:
branches:
- 'master'
- 'release-**'
paths:
- '.github/workflows/**'
pull_request:
paths:
- '.github/workflows/**'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
actionlint:
name: Lint Github Actions Workflows
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- uses: actions/checkout@v6
- name: Get changed Github actions
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
list-files: shell
- name: Setup actionlint
if: steps.changes.outputs.actionlint == 'true'
id: download
shell: bash
run: |
if command -v actionlint; then
echo "executable=actionlint" >> "$GITHUB_OUTPUT"
else
# script sets executable output, for details see: https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash)
fi
- name: Run actionlint
if: steps.changes.outputs.actionlint == 'true'
run: ${{ steps.download.outputs.executable }} --color --verbose ${{ steps.changes.outputs.actionlint_files }}

View File

@@ -61,6 +61,9 @@ jobs:
services:
mysql:
image: ${{ matrix.version.image }}
# disposable CI database: a RAM-backed datadir makes the ~90k fsyncs of a full
# migration chain free (measured: setup-db! 59s -> 16s)
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -117,22 +120,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-mysql:
if: ${{ !inputs.skip }}
@@ -187,6 +179,9 @@ jobs:
services:
mysql:
image: ${{ matrix.version.image }}
# disposable CI database: a RAM-backed datadir makes the ~90k fsyncs of a full
# migration chain free (measured: setup-db! 59s -> 16s)
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -251,22 +246,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-postgres:
if: ${{ !inputs.skip }}
@@ -363,22 +347,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
app-db-tests-result:
needs:

View File

@@ -99,6 +99,36 @@ jobs:
)
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- name: "!! double-backport and triple-backport label deprecation warning !!"
if:
|
github.event.pull_request.merged == true && (
github.event.action == 'closed' && (
contains(github.event.pull_request.labels.*.name, 'double-backport') ||
contains(github.event.pull_request.labels.*.name, 'triple-backport')
) ||
github.event.action == 'labeled' && (
github.event.label.name == 'double-backport' ||
github.event.label.name == 'triple-backport'
)
)
env:
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
run: |
msg="🚨🚨 Backport failed because \`double-backport\` and \`triple-backport\` labels are **deprecated**! Use the \`backport\` label to port changes to all supported releases instead. 🚨🚨"
gh api \
--silent \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
/repos/"$REPO"/issues/"$PR"/comments \
-f "body=$msg"
echo "::error::$msg"
exit 1
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -110,29 +140,6 @@ jobs:
pull-request-number: ${{ github.event.pull_request.number }}
base-ref: ${{ github.event.pull_request.base.ref }}
- name: how many backports?
id: how-many-backports
uses: actions/github-script@v9
with:
script: | # js
const labels = [
...${{ toJson(github.event.pull_request.labels) }},
{ name:"${{ github.event.label.name }}" }
];
const backportLabelNames = labels.filter(label => label.name.includes('backport')).map(label => label.name);
let result = 1;
if (backportLabelNames.includes("triple-backport")) {
result = 3;
} else if (backportLabelNames.includes("double-backport")) {
result = 2;
}
console.log("backports", result);
return result;
- name: get release versions
id: get-latest-release-version
uses: actions/github-script@v9
@@ -157,21 +164,3 @@ jobs:
backport-commit: ${{ steps.find_commit.outputs.commit }}
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
github-token-2: ${{ secrets.GITHUB_TOKEN }}
- name: Double Backport
if: ${{ always() && fromJson(steps.how-many-backports.outputs.result) >= 2 }}
uses: ./.github/actions/create-backport
with:
target-version: ${{ fromJson(steps.get-latest-release-version.outputs.result).secondLatest }}
backport-commit: ${{ steps.find_commit.outputs.commit }}
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
github-token-2: ${{ secrets.GITHUB_TOKEN }}
- name: Triple Backport
if: ${{ always() && fromJson(steps.how-many-backports.outputs.result) >= 3 }}
uses: ./.github/actions/create-backport
with:
target-version: ${{ fromJson(steps.get-latest-release-version.outputs.result).thirdLatest }}
backport-commit: ${{ steps.find_commit.outputs.commit }}
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
github-token-2: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -24,8 +24,8 @@ jobs:
- uses: actions/checkout@v6
- id: get-modules
run: |
modules=$(ls -1d src/metabase/*/ | xargs -n1 basename | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules" >> $GITHUB_OUTPUT
modules=$(for d in src/metabase/*/; do basename "$d"; done | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules" >> "$GITHUB_OUTPUT"
be-linter-cloverage:
needs: setup-oss
@@ -71,8 +71,8 @@ jobs:
- uses: actions/checkout@v6
- id: get-modules
run: |
modules=$(ls -1d enterprise/backend/src/metabase_enterprise/*/ | xargs -n1 basename | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules" >> $GITHUB_OUTPUT
modules=$(for d in enterprise/backend/src/metabase_enterprise/*/; do basename "$d"; done | tr '_' '-' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules" >> "$GITHUB_OUTPUT"
be-linter-cloverage-enterprise:
needs: setup-ee

View File

@@ -191,12 +191,51 @@ jobs:
# below can link failures to this exact job. Best-effort.
uses: ./.github/actions/resolve-job-id
# Granular rerun: on a re-run of a failed job, restore the list of tests
# that failed on the previous attempt (uploaded below) so the test step
# can target only those instead of the whole partition. Best-effort: a
# missing/empty artifact leaves no file and the run falls back to the full
# partition. First attempts skip this entirely.
#
# Scoped to the IMMEDIATELY preceding attempt: an attempt that passed, or
# that failed with nothing attributable to a var, uploads no artifact, and
# a narrow set must not outlive it. Reading `attempt - 1` means "only what
# the last attempt actually recorded" — anything else falls back to the
# full partition. Workflow expressions have no arithmetic, so the shell
# computes it.
- name: Resolve previous attempt
if: ${{ github.run_attempt != '1' }}
run: echo "RERUN_PREV_ATTEMPT=$((GITHUB_RUN_ATTEMPT - 1))" >> "$GITHUB_ENV"
# `pattern` (not `name`) because the artifact is legitimately absent whenever the
# previous attempt recorded nothing: `name` treats that as an error, `pattern`
# downloads nothing and moves on. The value is a literal — no glob metacharacters —
# so it matches exactly one artifact, which lands at `target/failed-tests`. A pattern
# that ever matched two would nest them under their artifact names instead, leaving
# the file absent and falling back to the full partition.
- name: Restore previously-failed tests
if: ${{ github.run_attempt != '1' }}
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: failed-tests-be-tests-java-${{ matrix.java-version }}-${{ matrix.job.test-group-name }}-attempt-${{ env.RERUN_PREV_ATTEMPT }}
path: target
- name: "Test Java ${{ matrix.java-version }} ${{ matrix.job.name }}"
id: run-java-tests
run: >-
clojure -X:dev:ci:test:${{ matrix.job.edition }}:${{ matrix.job.edition }}-dev
:exclude-tags '[:mb/driver-tests ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
${{ matrix.job.test-args }}
# On a rerun with a non-empty failed-tests file, target only those vars
# via hawk `:only`, dropping the tag/partition args — the listed vars are
# exactly this job's failures, already filtered and partitioned on the
# first attempt. Otherwise run exactly as before. Quote the file so the
# whole `[ns/var ...]` vector reaches clojure -X as a single argument.
run: |
BASE="clojure -X:dev:ci:test:${{ matrix.job.edition }}:${{ matrix.job.edition }}-dev"
if [ "${{ 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)"
else
$BASE :exclude-tags '[:mb/driver-tests ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]' ${{ matrix.job.test-args }}
fi
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
continue-on-error: true
@@ -209,6 +248,29 @@ jobs:
test-suite: be-tests-java-${{ matrix.java-version }}-${{ matrix.job.test-group-name }}
adapter: backend
# Granular rerun: record which tests failed so a re-run of this job targets
# only those (restored above). Reuses the backend JUnit adapter from the
# ci-conductor sources the report step just prepared. Best-effort: writes
# nothing unless every failure is attributable to a var, so an
# unattributable failure falls back to a full partition rerun.
- name: Collect failed tests for rerun
if: ${{ !cancelled() && steps.run-java-tests.outcome == 'failure' }}
continue-on-error: true
# Clear the restored set first, so a collector that dies before deciding
# can't leave the previous attempt's file to be uploaded as this one's.
run: |
rm -f target/failed-tests
bun release/ci-conductor/src/collect-failed-backend.ts
- name: Upload failed tests for rerun
if: ${{ !cancelled() && steps.run-java-tests.outcome == 'failure' }}
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: failed-tests-be-tests-java-${{ matrix.java-version }}-${{ matrix.job.test-group-name }}-attempt-${{ github.run_attempt }}
path: target/failed-tests
if-no-files-found: ignore
- name: Quarantine gate
if: ${{ !cancelled() && steps.run-java-tests.outcome == 'failure' }}
continue-on-error: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
@@ -219,19 +281,6 @@ jobs:
test-suite: be-tests-java-${{ matrix.java-version }}-${{ matrix.job.test-group-name }}
enforce-quarantine: ${{ vars.QUARANTINE_ENGINE == 'ci-conductor' }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: be-tests-java-${{ matrix.java-version }}-${{ matrix.job.edition }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-java-tests.outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Publish Test Report (JUnit)
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
if: failure()

View File

@@ -5,10 +5,10 @@ on:
jobs:
check-source-branch:
if: github.head_ref == 'master'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- name: Block master branch PRs
if: github.head_ref == 'master'
run: |
echo "PRs from `master` branches are not allowed, please use a new branch name for your PR."
echo "PRs from \`master\` branches are not allowed, please use a new branch name for your PR."
exit 1

View File

@@ -1,270 +0,0 @@
# Builds a fresh release JAR from scratch at a given commit.
#
# You should almost never have to use this. The normal release flow relies on a
# commit that has already been built and tested on its branch — see
# `tag-for-release.yml`, which tags that existing JAR. Building from scratch
# here is only for exceptional cases.
name: Release 0 - Build Release Artifact
run-name: Build ${{ inputs.version }}
on:
workflow_dispatch:
inputs:
version:
description: 'Metabase version (e.g. v0.46.3)'
type: string
required: true
commit:
description: 'A full-length commit SHA-1 hash'
required: true
jobs:
start-message:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Send build start message
uses: actions/github-script@v9
env:
SLACK_RELEASE_CHANNEL: ${{ vars.SLACK_RELEASE_CHANNEL }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const { sendPreReleaseMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
await sendPreReleaseMessage({
github,
owner: context.repo.owner,
repo: context.repo.repo,
version: '${{ inputs.version }}',
runId: '${{ github.run_id }}',
releaseSha: '${{ inputs.commit }}',
userName: '${{ github.actor }}',
channelName: '${{ vars.SLACK_RELEASE_CHANNEL }}',
}).catch(console.error);
trigger-milestone-check:
# Posts a Slack FYI about issues that still need review for this release.
# Informational only — nothing depends on it, so it never blocks the release.
needs: [check-version, check-commit]
permissions:
contents: read
issues: read
uses: ./.github/workflows/release-milestone-check.yml
secrets: inherit
with:
version: ${{ inputs.version }}
commit: ${{ inputs.commit }}
check-version:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
outputs:
ee: ${{ fromJson(steps.canonical_version.outputs.result).ee }}
oss: ${{ fromJson(steps.canonical_version.outputs.result).oss }}
steps:
- name: Fail early on the incorrect version format
if: ${{ !(startsWith(inputs.version,'v0.') || startsWith(inputs.version,'v1.')) }}
run: |
echo "The version format is invalid!"
echo "It must start with either 'v0.' or 'v1.'."
echo "Please, try again."
exit 1
- uses: actions/checkout@v6
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Get Release Version
uses: actions/github-script@v9
id: canonical_version
with:
script: | # js
const { isValidVersionString, getCanonicalVersion, hasBeenReleased } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
if (!isValidVersionString(version)) {
throw new Error("The version format is invalid! It must start with either 'v0.' or 'v1.'.");
}
const versions = {
ee: getCanonicalVersion(version, 'ee'),
oss: getCanonicalVersion(version, 'oss'),
};
const released = await hasBeenReleased({
github,
owner: context.repo.owner,
repo: context.repo.repo,
version: versions.oss,
});
if (released) {
throw new Error("This version has already been released!", version);
}
return versions;
check-commit:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
steps:
- name: Check out the code to verify the release branch
uses: actions/checkout@v6
with:
fetch-depth: 0 # IMPORTANT! to get all the branches
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Get Release Branch
uses: actions/github-script@v9
id: release_branch
with:
result-encoding: string
script: | # js
const { getReleaseBranch } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
const releaseBranch = getReleaseBranch(version);
return releaseBranch;
- name: Ensure that the specified commit exists in the ${{ steps.release_branch.outputs.result }} release branch
run: | # bash
RELEASE_BRANCH=${{ steps.release_branch.outputs.result }}
git checkout $RELEASE_BRANCH
git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch" \
|| (echo "Commit not found in correct release branch" && exit 1)
git checkout -
get-build-requirements:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
outputs:
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
build_process: ${{ fromJson(steps.dependencies.outputs.result).build_process }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: |
release
.github
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Get build dependencies
uses: actions/github-script@v9
id: dependencies
with:
script: | # js
const { getBuildRequirements, getMajorVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
const requirements = getBuildRequirements(version);
const build_process = getMajorVersion(version) < 46 ? 'legacy' : 'new';
console.log('Build process:', build_process);
return {
java_version: requirements.java,
node_version: requirements.node,
build_process,
};
manage-milestones:
# Close the milestone we're about to ship and open the next one BEFORE building,
# so any PRs that merge while the build is in flight are tagged with the next
# milestone (not the one about to be released).
#
# Sequenced after trigger-milestone-check so that the check sees the milestone
# in its open state. milestone-check is informational and must never block, so
# we run regardless of its outcome; only preflight failures suppress this.
needs: [check-version, check-commit, trigger-milestone-check]
if: ${{ !cancelled() && needs.check-version.result == 'success' && needs.check-commit.result == 'success' }}
permissions:
contents: read
issues: write
uses: ./.github/workflows/release-milestone-manage.yml
with:
version: ${{ inputs.version }}
build-uberjar-for-release:
needs: [check-version, check-commit, get-build-requirements]
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 50
strategy:
matrix:
edition: [oss, ee]
env:
INTERACTIVE: false
steps:
- name: Get correct version number
run: | # bash
if [ '${{matrix.edition}}' == 'ee' ]; then
version='${{ needs.check-version.outputs.ee }}'
else
version='${{ needs.check-version.outputs.oss }}'
fi
echo "VERSION=$version" >> $GITHUB_ENV
- name: Setup Java ${{ needs.get-build-requirements.outputs.java_version }}
uses: actions/setup-java@v5.2.0
with:
distribution: 'temurin'
java-version: ${{ needs.get-build-requirements.outputs.java_version }}
- name: Setup Node.js ${{ needs.get-build-requirements.outputs.node_version }}
uses: actions/setup-node@v6.4.0
with:
node-version: ${{ needs.get-build-requirements.outputs.node_version }}.x
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.commit }}
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build Metabase
if: ${{ needs.get-build-requirements.outputs.build_process != 'legacy' }}
run: ./bin/build.sh :edition :${{ matrix.edition }} :version $VERSION
- name: Build Metabase (legacy)
if: ${{ needs.get-build-requirements.outputs.build_process == 'legacy' }}
run: cd ./bin/build-mb && clojure -X build/build! :edition :${{ matrix.edition }} :version $VERSION
- name: Store commit's SHA-1 hash
run: echo ${{ inputs.commit }} > COMMIT-ID
shell: bash
- run: mv ./target/uberjar/metabase.jar ./metabase.jar
- name: Calculate SHA256 checksum
run: sha256sum ./metabase.jar > SHA256.sum
shell: bash
- name: Upload JARs as artifact
uses: actions/upload-artifact@v7
with:
name: metabase-release-${{ matrix.edition }}-${{ inputs.commit }}-uberjar
path: |
./metabase.jar
./COMMIT-ID
./SHA256.sum
run-pre-release-tests:
name: Run pre-release tests for ${{ inputs.version }}
needs: build-uberjar-for-release
uses: ./.github/workflows/pre-release.yml
secrets: inherit
with:
commit: ${{ inputs.commit }}
version: ${{ inputs.version }}

View File

@@ -93,10 +93,10 @@ jobs:
":dev:ee:ee-dev:drivers:drivers-dev:test:eastwood" # backend.yml (eastwood + eastwood/test)
":dev:ci:ee:ee-dev:drivers:drivers-dev:test" # test-driver.yml (ee) -> covers data.json:2.4.0
":dev:ci:oss:oss-dev:drivers:drivers-dev:test" # test-driver.yml (oss)
":dev:ci:test:ee:ee-dev" # backend.yml matrix (ee) + generative-query-test.yml
":dev:ci:test:ee:ee-dev" # backend.yml matrix (ee)
":dev:ci:test:oss:oss-dev" # backend.yml matrix (oss)
":dev:ci:ee:ee-dev:test:cloverage" # backend-cloverage.yml
":dev:drivers:drivers-dev:test" # reset-cloud-test-dataset.yml + presto script
":dev:drivers:drivers-dev:test" # reset-cloud-test-dataset.yml
":dev:drivers:build:build-dev:build-test:ci" # build-scripts.yml
":ee:drivers:check" # backend.yml (check; git dep athos/clj-check)
":ee:doc" # docs-generate-base.yml
@@ -135,7 +135,7 @@ jobs:
run: |
BUN_CACHE_DIR=$(bun pm cache)
echo "Bun cache directory: $BUN_CACHE_DIR"
echo "dir=$BUN_CACHE_DIR" >> $GITHUB_OUTPUT
echo "dir=$BUN_CACHE_DIR" >> "$GITHUB_OUTPUT"
- name: Ensure that Cypress executable is ready
run: |

View File

@@ -1,39 +0,0 @@
name: "CodeQL"
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
codeql: ${{ steps.changes.outputs.codeql }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
analyze:
needs: files-changed
if: needs.files-changed.outputs.codeql == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: javascript
config-file: ./.github/codeql/codeql-config.yml
trap-caching: false
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

View File

@@ -27,7 +27,7 @@ jobs:
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
run: echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
- name: Run codespell check
id: codespell_check

View File

@@ -16,8 +16,8 @@ on:
commit:
type: string
release:
type: string
default: 'false'
type: boolean
default: false
build-args:
type: string
workflow_dispatch:
@@ -40,11 +40,8 @@ on:
type: string
release:
description: Whether this is a release artifact (uses metabase-release- prefix)
type: choice
options:
- "false"
- "true"
default: "false"
type: boolean
default: false
jobs:
containerize:
@@ -88,8 +85,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
username: ${{ inputs.release && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
if: contains(steps.platforms.outputs.result, 'arm')
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
@@ -126,8 +123,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
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 }}
- name: Launch container
@@ -146,8 +143,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ inputs.release == 'true' && secrets.DOCKERHUB_RELEASE_TOKEN || secrets.DOCKERHUB_TOKEN }}
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 }}
- name: Launch container

View File

@@ -3,6 +3,10 @@ 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:
@@ -59,7 +63,7 @@ jobs:
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"
release: false
build-args: |
GIT_COMMIT_SHA=${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}

View File

@@ -103,6 +103,8 @@ jobs:
services:
mysql:
image: mysql:8.0
# disposable CI database: a RAM-backed datadir makes migration fsyncs free
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -129,6 +131,8 @@ jobs:
services:
mysql:
image: mysql:9
# disposable CI database: a RAM-backed datadir makes migration fsyncs free
options: --tmpfs /var/lib/mysql:rw
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: mb_test
@@ -183,6 +187,8 @@ jobs:
services:
mariadb:
image: metabase/mariadb:10.6 # https://github.com/metabase/metabase-mariadb-docker-image
# disposable CI database: a RAM-backed datadir makes migration fsyncs free
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -208,6 +214,8 @@ jobs:
services:
mariadb:
image: mariadb:latest
# disposable CI database: a RAM-backed datadir makes migration fsyncs free
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:

View File

@@ -34,18 +34,18 @@ jobs:
const lastUpdated = config.metadata?.last_updated || 'unknown';
const reason = config.metadata?.reason || 'No reason provided';
// "skip" is the only status CI reads: the driver is not run at all. A driver that
// runs but must not gate is expressed as a suite-level rule in CI Conductor, which
// reports it in its own UI.
const skipped = drivers.filter(d => d.status === 'skip').map(d => d.name);
const info = drivers.filter(d => d.status === 'info').map(d => d.name);
if (skipped.length === 0 && info.length === 0) {
console.log('No drivers are disabled or collecting data. Skipping Slack notification.');
if (skipped.length === 0) {
console.log('No drivers are disabled. Skipping Slack notification.');
return;
}
const list = (names) => names.map(d => `\`${d}\``).join(' ');
const lines = [];
if (skipped.length) lines.push(`:no_entry: *Disabled (not run):* ${list(skipped)}`);
if (info.length) lines.push(`:information_source: *Collecting data (non-blocking):* ${list(info)}`);
const lines = [`:no_entry: *Disabled (not run):* ${list(skipped)}`];
if (!process.env.SLACK_BOT_TOKEN) {
throw new Error('SLACK_BOT_TOKEN is required');

View File

@@ -79,10 +79,10 @@ jobs:
id: check_changes
run: |
if git diff --quiet docs/; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "changes=false" >> "$GITHUB_OUTPUT"
echo "No changes detected in docs directory"
else
echo "changes=true" >> $GITHUB_OUTPUT
echo "changes=true" >> "$GITHUB_OUTPUT"
echo "Changes detected in docs directory"
fi
@@ -109,7 +109,7 @@ jobs:
git commit -m "Update generated docs (${{ inputs.target_branch }})" --no-verify
git push -u origin "$BRANCH_NAME"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "Successfully created and pushed branch"
- name: Create pull request
@@ -155,7 +155,7 @@ jobs:
break
else
echo "PR creation failed on attempt $i"
if [ $i -eq 3 ]; then
if [ "$i" -eq 3 ]; then
echo "All attempts failed. Exiting with error."
exit 1
fi
@@ -172,22 +172,24 @@ jobs:
- name: Workflow summary
if: always()
run: |
if [ "${{ steps.check_changes.outputs.changes }}" == "true" ]; then
if [ "${{ steps.commit_and_push.outcome }}" == "success" ]; then
if [ "${{ steps.create_pr.outcome }}" == "success" ]; then
echo "## ✅ Documentation Updated Successfully" >> $GITHUB_STEP_SUMMARY
echo "- Branch: \`${{ steps.commit_and_push.outputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- PR created targeting **${{ inputs.target_branch }}**" >> $GITHUB_STEP_SUMMARY
{
if [ "${{ steps.check_changes.outputs.changes }}" == "true" ]; then
if [ "${{ steps.commit_and_push.outcome }}" == "success" ]; then
if [ "${{ steps.create_pr.outcome }}" == "success" ]; then
echo "## ✅ Documentation Updated Successfully"
echo "- Branch: \`${{ steps.commit_and_push.outputs.branch_name }}\`"
echo "- PR created targeting **${{ inputs.target_branch }}**"
else
echo "## ⚠️ Branch Created but PR Failed"
echo "- Branch: \`${{ steps.commit_and_push.outputs.branch_name }}\`"
echo "- Manual PR creation required"
fi
else
echo "## ⚠️ Branch Created but PR Failed" >> $GITHUB_STEP_SUMMARY
echo "- Branch: \`${{ steps.commit_and_push.outputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Manual PR creation required" >> $GITHUB_STEP_SUMMARY
echo "## ❌ Failed to Create Branch"
echo "- Check workflow logs for details"
fi
else
echo "## ❌ Failed to Create Branch" >> $GITHUB_STEP_SUMMARY
echo "- Check workflow logs for details" >> $GITHUB_STEP_SUMMARY
echo "## ℹ️ No Changes Detected"
echo "- Documentation is up to date on **${{ inputs.target_branch }}**"
fi
else
echo "## ℹ️ No Changes Detected" >> $GITHUB_STEP_SUMMARY
echo "- Documentation is up to date on **${{ inputs.target_branch }}**" >> $GITHUB_STEP_SUMMARY
fi
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -27,7 +27,7 @@ jobs:
- name: Run link checker
id: link-check
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
fail: true
args: docs --config ./.lychee/config.toml --offline

View File

@@ -69,6 +69,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-athena-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-bigquery-cloud-sdk-ee:
@@ -91,6 +92,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-druid-ee:
@@ -113,6 +115,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mariadb-10-6-ee:
@@ -142,6 +145,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-10-6-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mariadb-latest-ee:
@@ -171,6 +175,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-latest-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mongo-4-4-ee:
@@ -193,6 +198,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-4-4-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mongo-4-4-ssl-ee:
@@ -227,6 +233,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-4-4-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mongo-5-0-ee:
@@ -249,6 +256,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-5-0-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mongo-5-0-ssl-ee:
@@ -283,6 +291,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-5-0-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mongo-latest-ee:
@@ -308,6 +317,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-latest-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-8-0-ee:
@@ -337,6 +347,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-8-0-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-9-ee:
@@ -366,6 +377,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-9-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-latest-ee:
@@ -404,6 +416,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-latest-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-oracle-18-4-ee:
@@ -430,6 +443,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-oracle-18-4-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-oracle-21-3-ee:
@@ -465,6 +479,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-oracle-21-3-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-postgres-ee:
@@ -495,6 +510,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-postgres-latest-ee:
@@ -528,6 +544,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-latest-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-presto-jdbc-ee:
@@ -558,7 +575,7 @@ jobs:
run: while ! nc -z localhost ${{ env.MB_PRESTO_JDBC_TEST_PORT }}; do sleep 0.1; done
timeout-minutes: 15
- name: Create temp cacerts file based on bundled JDK one
run: cp $JAVA_HOME/lib/security/cacerts /tmp/cacerts-with-presto-ssl.jks
run: cp "$JAVA_HOME/lib/security/cacerts" /tmp/cacerts-with-presto-ssl.jks
- name: Capture Presto server self signed CA
run: |
while [[ ! -s /tmp/presto-ssl-ca.pem ]];
@@ -577,6 +594,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-presto-jdbc-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-redshift-ee:
@@ -596,6 +614,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-redshift-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-snowflake-ee:
@@ -627,6 +646,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-snowflake-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-sparksql-ee:
@@ -647,6 +667,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sparksql-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-sqlite-ee:
@@ -662,6 +683,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlite-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-sqlserver-2017-ee:
@@ -689,6 +711,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlserver-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-sqlserver-2022-ee:
@@ -716,6 +739,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlserver-2022-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-starburst-ee:
@@ -741,6 +765,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-starburst-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
login-to-ecr:
@@ -764,9 +789,11 @@ jobs:
REGISTRY="$(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com"
echo "docker_username=AWS" >> $GITHUB_OUTPUT
echo "docker_password=$PASSWORD" >> $GITHUB_OUTPUT
echo "registry=$REGISTRY" >> $GITHUB_OUTPUT
{
echo "docker_username=AWS"
echo "docker_password=$PASSWORD"
echo "registry=$REGISTRY"
} >> "$GITHUB_OUTPUT"
outputs:
registry: ${{ steps.login-ecr.outputs.registry }}
docker_username: ${{ steps.login-ecr.outputs.docker_username }}
@@ -796,4 +823,5 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-vertica-ee'
granular-rerun: 'false'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"

View File

@@ -45,26 +45,6 @@ jobs:
sqlite-should-run: ${{ steps.driver-decisions.outputs.sqlite-should-run }}
sqlserver-should-run: ${{ steps.driver-decisions.outputs.sqlserver-should-run }}
vertica-should-run: ${{ steps.driver-decisions.outputs.vertica-should-run }}
# Per-driver configured status from ci-test-config (skip | info | required)
h2-status: ${{ steps.driver-decisions.outputs.h2-status }}
athena-status: ${{ steps.driver-decisions.outputs.athena-status }}
bigquery-status: ${{ steps.driver-decisions.outputs.bigquery-status }}
clickhouse-status: ${{ steps.driver-decisions.outputs.clickhouse-status }}
databricks-status: ${{ steps.driver-decisions.outputs.databricks-status }}
druid-jdbc-status: ${{ steps.driver-decisions.outputs.druid-jdbc-status }}
mongo-status: ${{ steps.driver-decisions.outputs.mongo-status }}
mongo-ssl-status: ${{ steps.driver-decisions.outputs.mongo-ssl-status }}
mongo-sharded-cluster-status: ${{ steps.driver-decisions.outputs.mongo-sharded-cluster-status }}
mysql-mariadb-status: ${{ steps.driver-decisions.outputs.mysql-mariadb-status }}
oracle-status: ${{ steps.driver-decisions.outputs.oracle-status }}
postgres-status: ${{ steps.driver-decisions.outputs.postgres-status }}
presto-jdbc-status: ${{ steps.driver-decisions.outputs.presto-jdbc-status }}
redshift-status: ${{ steps.driver-decisions.outputs.redshift-status }}
snowflake-status: ${{ steps.driver-decisions.outputs.snowflake-status }}
sparksql-status: ${{ steps.driver-decisions.outputs.sparksql-status }}
sqlite-status: ${{ steps.driver-decisions.outputs.sqlite-status }}
sqlserver-status: ${{ steps.driver-decisions.outputs.sqlserver-status }}
vertica-status: ${{ steps.driver-decisions.outputs.vertica-status }}
# Per-driver quarantine conflict flags (set when driver is quarantined but has file changes)
athena-quarantine-conflict: ${{ steps.driver-decisions.outputs.athena-quarantine-conflict }}
bigquery-quarantine-conflict: ${{ steps.driver-decisions.outputs.bigquery-quarantine-conflict }}
@@ -86,28 +66,6 @@ jobs:
sqlserver-quarantine-conflict: ${{ steps.driver-decisions.outputs.sqlserver-quarantine-conflict }}
vertica-quarantine-conflict: ${{ steps.driver-decisions.outputs.vertica-quarantine-conflict }}
steps:
# Up front (before any driver job), drop info-driver comments from older commits so a
# superseded comment disappears immediately rather than waiting for this run's drivers.
# Keeps this commit's comment (if any) so a re-run doesn't flicker.
- name: Remove stale info-driver comments
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v9
continue-on-error: true
with:
script: | # js
const pr = context.payload.pull_request;
if (!pr) return;
const { owner, repo } = context.repo;
const PREFIX = "<!-- info-driver-result";
const keep = `<!-- info-driver-results:${pr.head.sha} -->`;
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: pr.number, per_page: 100,
});
for (const c of comments) {
if (c.body.includes(PREFIX) && !c.body.includes(keep)) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }).catch(() => {});
}
}
- name: Checkout repository
uses: actions/checkout@v6
- name: Fetch base branch
@@ -180,20 +138,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.athena-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.athena-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-bigquery-cloud-sdk-ee:
needs: determine-driver-skips
@@ -256,23 +200,8 @@ jobs:
with:
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
artifact-suffix: ${{ matrix.job.name }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.bigquery-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.bigquery-status }}
report-variant: ${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -344,22 +273,8 @@ jobs:
with:
junit-name: 'be-tests-clickhouse-ee'
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
artifact-suffix: ${{ matrix.job.name }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.clickhouse-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.clickhouse-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -404,20 +319,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.druid-jdbc-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.druid-jdbc-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-databricks-ee:
needs: determine-driver-skips
@@ -477,23 +378,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-ee'
artifact-suffix: ${{ matrix.job.name }}
test-args: ${{ matrix.job.test-args }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.databricks-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.databricks-status }}
report-variant: ${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-databricks-multi-catalog-ee:
needs: determine-driver-skips
@@ -554,23 +441,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-multi-catalog-ee'
artifact-suffix: ${{ matrix.job.name }}
test-args: ${{ matrix.job.test-args }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.databricks-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.databricks-status }}
report-variant: ${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-h2:
needs: determine-driver-skips
@@ -591,20 +464,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.h2-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.h2-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-h2-oss:
needs: determine-driver-skips
@@ -626,20 +485,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.h2-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.h2-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-mysql-mariadb:
needs: determine-driver-skips
@@ -696,6 +541,8 @@ jobs:
services:
mysql:
image: ${{ matrix.version.image }}
# disposable CI database: a RAM-backed datadir makes DDL/insert fsyncs free
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -764,25 +611,11 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
logs-artifact-suffix: ${{ matrix.version.name }}
artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.mysql-mariadb-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.mysql-mariadb-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -851,22 +684,8 @@ jobs:
with:
junit-name: ${{ matrix.version.junit-name }}
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.mongo-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.mongo-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -939,20 +758,6 @@ jobs:
:exclude-tags '[:mongo-sharded-cluster-tests :mb/transforms-python-test]'
:only-tags [:mb/driver-tests]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.mongo-ssl-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.mongo-ssl-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-mongo-sharded-cluster-ee:
needs: determine-driver-skips
@@ -988,20 +793,6 @@ jobs:
test-args: >-
:only metabase.driver.mongo.sharded-cluster-test
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-oracle:
needs: determine-driver-skips
@@ -1072,25 +863,11 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
logs-artifact-suffix: ${{ matrix.version.name }}
artifact-suffix: ${{ matrix.version.name }}
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.oracle-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.oracle-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-postgres:
needs: determine-driver-skips
@@ -1180,26 +957,11 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
logs-artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.postgres-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.postgres-status }}
report-variant: ${{ matrix.version.name }}-${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -1277,20 +1039,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.presto-jdbc-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.presto-jdbc-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-redshift-ee:
needs: determine-driver-skips
@@ -1363,23 +1111,8 @@ jobs:
junit-name: 'be-tests-redshift-ee'
test-args: >-
${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
artifact-suffix: ${{ matrix.job.name }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.redshift-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: ${{ !matrix.job.skip && !cancelled() }}
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.redshift-status }}
report-variant: ${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -1451,23 +1184,8 @@ jobs:
with:
junit-name: 'be-tests-snowflake-ee'
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.job.name }}
artifact-suffix: ${{ matrix.job.name }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.snowflake-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.snowflake-status }}
report-variant: ${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -1510,20 +1228,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.sparksql-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.sparksql-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-sqlite-ee:
needs: determine-driver-skips
@@ -1555,20 +1259,6 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.sqlite-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.sqlite-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-sqlserver:
needs: determine-driver-skips
@@ -1633,22 +1323,8 @@ jobs:
with:
junit-name: ${{ matrix.version.junit-name }}
test-args: ${{ matrix.job.test-args }}
logs-artifact-suffix: ${{ matrix.version.name }}
artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.sqlserver-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.sqlserver-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Cleanup python-runner
if: ${{ always() && matrix.job.needs-python-runner == true }}
uses: ./.github/actions/cleanup-python-runner
@@ -1683,16 +1359,6 @@ jobs:
# junit-name: 'be-tests-starburst-ee'
# test-args: >-
# :only-tags [:mb/driver-tests]
# - name: Upload Test Results
# uses: ./.github/actions/upload-test-results
# if: always()
# with:
# input-path: ./target/junit/
# output-name: ${{ github.job }}
# bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
# role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
# aws-region: ${{ vars.AWS_REGION }}
# trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
login-to-ecr:
needs: determine-driver-skips
@@ -1766,34 +1432,13 @@ jobs:
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
driver-status: ${{ needs.determine-driver-skips.outputs.vertica-status }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.vertica-status }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
drivers-tests-result:
needs:
# Supplies each driver's configured status (info/skip/required) so this gate can ignore
# "info" drivers regardless of their result -- including cancelled or timed-out runs, which
# the per-job continue-on-error cannot force green because the controlling step never
# completes. Not a driver job; it is filtered out of the pass/fail check below.
- determine-driver-skips
# if you add a driver job, you must add it to this list for it to be a required check
- be-tests-h2
- be-tests-athena-ee
# Listed unconditionally; ci-test-config controls their effect via per-driver status.
# An "info" driver is reported as passing, so it never blocks this check; flip it to
# "required" in the config to make a real failure gate, with no change here.
- be-tests-snowflake-ee
- be-tests-bigquery-cloud-sdk-ee
- be-tests-databricks-ee
@@ -1822,46 +1467,18 @@ jobs:
uses: actions/github-script@v9
env:
needs: ${{ toJson(needs) }}
# Job name -> configured driver status, from determine-driver-skips. An "info" driver is
# data-collection only and must never gate; a cancelled/timed-out info job lands here as
# cancelled/failure (its own continue-on-error can't force it green), so we drop info jobs
# from the gate entirely. Empty/unknown status falls back to "required" (gates as before).
statuses: |
{
"be-tests-athena-ee": "${{ needs.determine-driver-skips.outputs.athena-status }}",
"be-tests-bigquery-cloud-sdk-ee": "${{ needs.determine-driver-skips.outputs.bigquery-status }}",
"be-tests-clickhouse-ee": "${{ needs.determine-driver-skips.outputs.clickhouse-status }}",
"be-tests-databricks-ee": "${{ needs.determine-driver-skips.outputs.databricks-status }}",
"be-tests-druid-jdbc-ee": "${{ needs.determine-driver-skips.outputs.druid-jdbc-status }}",
"be-tests-h2": "${{ needs.determine-driver-skips.outputs.h2-status }}",
"be-tests-mongo": "${{ needs.determine-driver-skips.outputs.mongo-status }}",
"be-tests-mongo-ssl": "${{ needs.determine-driver-skips.outputs.mongo-ssl-status }}",
"be-tests-mongo-sharded-cluster-ee": "${{ needs.determine-driver-skips.outputs.mongo-sharded-cluster-status }}",
"be-tests-mysql-mariadb": "${{ needs.determine-driver-skips.outputs.mysql-mariadb-status }}",
"be-tests-oracle": "${{ needs.determine-driver-skips.outputs.oracle-status }}",
"be-tests-postgres": "${{ needs.determine-driver-skips.outputs.postgres-status }}",
"be-tests-presto-jdbc-ee": "${{ needs.determine-driver-skips.outputs.presto-jdbc-status }}",
"be-tests-redshift-ee": "${{ needs.determine-driver-skips.outputs.redshift-status }}",
"be-tests-snowflake-ee": "${{ needs.determine-driver-skips.outputs.snowflake-status }}",
"be-tests-sparksql-ee": "${{ needs.determine-driver-skips.outputs.sparksql-status }}",
"be-tests-sqlite-ee": "${{ needs.determine-driver-skips.outputs.sqlite-status }}",
"be-tests-sqlserver": "${{ needs.determine-driver-skips.outputs.sqlserver-status }}"
}
with:
script: | # js
const needs = JSON.parse(process.env.needs);
const statuses = JSON.parse(process.env.statuses);
// Drop the determine-driver-skips bookkeeping job, then drop "info" drivers: they are
// non-gating, so their result (including cancelled/timed-out) must not fail this check.
// Drop the determine-driver-skips bookkeeping job; every remaining job is a driver
// job whose result gates this check.
const jobs = Object.entries(needs)
.filter(([jobName]) => jobName !== 'determine-driver-skips')
.map(([jobName, jobValues]) => ({
name: jobName,
result: jobValues.result,
status: statuses[jobName] || 'required'
}))
.filter(job => job.status !== 'info');
result: jobValues.result
}));
// are all jobs skipped or successful?
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {

View File

@@ -48,6 +48,18 @@ jobs:
name: instrumented-uberjar
path: target/uberjar/metabase.jar
retention-days: 1
# Regenerate the OpenAPI spec from this SHA's defendpoint definitions so
# route normalization in the merge job matches against the endpoints
# that actually served this run, not the checked-in file (which is only
# refreshed by the label-gated openapi-check workflow and drifts).
- name: Generate OpenAPI route table
run: bun run generate-openapi
- name: Upload OpenAPI route table
uses: actions/upload-artifact@v7
with:
name: openapi-spec
path: resources/openapi/openapi.json
retention-days: 1
# Fixed-N sharding — coverage doesn't need the PR matrix-builder's tag/edition
# groups, just an even split of the regular spec set. The shard count lives in
@@ -130,6 +142,15 @@ jobs:
- name: Unzip previous manifest
if: steps.prev.outputs.found == 'true'
run: unzip -o prev-manifest.zip -d prev-manifest
# Overwrites the checked-out resources/openapi/openapi.json with the one
# generated from this run's SHA. If the artifact is somehow missing, the
# checked-in file is still there and the builder falls back to it.
- name: Download OpenAPI route table
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: openapi-spec
path: resources/openapi
# GITHUB_SHA (the master commit this nightly built from) is stamped into
# the manifest as `builtAt`, so a PR can later pick the manifest closest
# to its own point in history. See resolve-coverage-manifest.mjs.
@@ -137,9 +158,25 @@ jobs:
env:
PREV_MANIFEST: ${{ steps.prev.outputs.found == 'true' && 'prev-manifest/spec-file-manifest.json' || '' }}
run: node e2e/coverage/build-coverage-manifest.mjs
- name: Upload manifest
# Manifest routes are stored table-independent; consumers match them
# against an endpoint spec at selection time. Ship this run's generated
# spec alongside so they can choose it over their checkout's copy.
# Copied into e2e/coverage so the artifact stays flat — the backfill
# step expects the manifests at the archive root.
- name: Include OpenAPI spec in artifact
run: |
if [ -f resources/openapi/openapi.json ]; then
cp resources/openapi/openapi.json e2e/coverage/openapi.json
fi
# All files share one artifact so the per-test manifest always travels
# with the spec manifest it was built from (backfill and PR-side
# resolution read them as siblings).
- name: Upload manifests
uses: actions/upload-artifact@v7
with:
name: spec-file-manifest
path: e2e/coverage/spec-file-manifest.json
path: |
e2e/coverage/spec-file-manifest.json
e2e/coverage/spec-test-manifest.json
e2e/coverage/openapi.json
retention-days: 30

View File

@@ -3,7 +3,7 @@ name: E2E Coverage Shard
# Reusable per-shard worker for the coverage manifest run. Trimmed extract of
# e2e-test.yml: runs one cypress-split shard against an instrumented build and
# uploads that shard's per-spec coverage. Intentionally omits the PR-only
# concerns (failed-spec retry, Trunk quarantine, S3 result upload, timing
# concerns (failed-spec retry, quarantine, S3 result upload, timing
# output) since this is a nightly, non-blocking job.
on:

View File

@@ -4,7 +4,7 @@ on:
workflow_call:
inputs:
chunks:
required: true
required: false
type: number
default: 30
specs:

View File

@@ -98,9 +98,9 @@ jobs:
run: |
SPEC='${{ inputs.spec }}'
if [[ "$SPEC" == e2e/test-component/scenarios/embedding-sdk/* ]]; then
echo "type=component" >> $GITHUB_OUTPUT
echo "type=component" >> "$GITHUB_OUTPUT"
else
echo "type=e2e" >> $GITHUB_OUTPUT
echo "type=e2e" >> "$GITHUB_OUTPUT"
fi
workflow-summary:
@@ -110,20 +110,33 @@ jobs:
timeout-minutes: 5
steps:
- name: Generate workflow summary
env:
BRANCH: ${{ github.ref_name }}
SPEC: ${{ inputs.spec }}
TEST_TYPE: ${{ needs.determine-test-type.outputs.type }}
BURN_IN: ${{ inputs.burn_in }}
GREP: ${{ inputs.grep }}
QA_DB: ${{ inputs.qa_db }}
ENABLE_NETWORK_THROTTLING: ${{ inputs.enable_network_throttling }}
FAIL_FAST: ${{ inputs.fail_fast }}
BUILD_JAR: ${{ inputs.build_jar }}
SENDER: ${{ github.event.sender.login }}
run: |
echo '**Inputs:**' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '- `branch`: ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
echo '- `spec`: ${{ inputs.spec }}' >> $GITHUB_STEP_SUMMARY
echo '- `test_type` (inferred): ${{ needs.determine-test-type.outputs.type }}' >> $GITHUB_STEP_SUMMARY
echo '- `burn_in`: ${{ inputs.burn_in }}' >> $GITHUB_STEP_SUMMARY
echo '- `grep`: "${{ inputs.grep }}"' >> $GITHUB_STEP_SUMMARY
echo '- `qa_db`: "${{ inputs.qa_db }}"' >> $GITHUB_STEP_SUMMARY
echo '- `enable_network_throttling`: "${{ inputs.enable_network_throttling }}"' >> $GITHUB_STEP_SUMMARY
echo '- `fail_fast`: "${{ inputs.fail_fast }}"' >> $GITHUB_STEP_SUMMARY
echo '- `build_jar`: "${{ inputs.build_jar }}"' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
{
echo '**Inputs:**'
echo ''
echo "- \`branch\`: $BRANCH"
echo "- \`spec\`: $SPEC"
echo "- \`test_type\` (inferred): $TEST_TYPE"
echo "- \`burn_in\`: $BURN_IN"
echo "- \`grep\`: \"$GREP\""
echo "- \`qa_db\`: \"$QA_DB\""
echo "- \`enable_network_throttling\`: \"$ENABLE_NETWORK_THROTTLING\""
echo "- \`fail_fast\`: \"$FAIL_FAST\""
echo "- \`build_jar\`: \"$BUILD_JAR\""
echo ''
echo "triggered by: @$SENDER"
} >> "$GITHUB_STEP_SUMMARY"
stress-test-flake-fix:
needs: [determine-test-type]

View File

@@ -63,14 +63,14 @@ jobs:
fi
echo "Changed e2e Cypress specs:"
echo "$FILES" | sed 's/^/ - /'
echo "$FILES" | awk '{ print " - " $0 }'
# Compact JSON array for the matrix, e.g. ["e2e/test/scenarios/a.cy.spec.js"].
SPECS_JSON=$(echo "$FILES" | jq -R . | jq -cs .)
echo "specs=${SPECS_JSON}" >> "$GITHUB_OUTPUT"
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Note: you can use the burn-50 or burn-100 labels to increase the burn-in count from the default of 20." >> $GITHUB_STEP_SUMMARY
echo "Note: you can use the burn-50 or burn-100 labels to increase the burn-in count from the default of 20." >> "$GITHUB_STEP_SUMMARY"
stress-test:
name: Stress test

View File

@@ -68,6 +68,7 @@ jobs:
# deleted in after:spec so recording is ~5-10% slowdown per shard and almost
# never yields an artifact. Failure screenshots are unaffected, and the
# stress-test workflows have their own config and keep video.
# Re-run attempts override this on the run steps below.
CYPRESS_VIDEO: "false"
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
@@ -173,6 +174,8 @@ jobs:
# do not write cypress-split summary to github
SPLIT_SUMMARY: false
SPEC_PATH: ${{ env.specs || inputs.specs }}
# Re-run attempts run only the previously-failed specs (env.specs), so record them.
CYPRESS_VIDEO: ${{ env.specs && 'true' || 'false' }}
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
shell: bash
run: |
@@ -189,6 +192,8 @@ jobs:
shell: bash
env:
SPEC_PATH: ${{ env.specs || inputs.specs }}
# Re-run attempts run only the previously-failed specs (env.specs), so record them.
CYPRESS_VIDEO: ${{ env.specs && 'true' || 'false' }}
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: |
echo "Running tests with specs: $SPEC_PATH"
@@ -211,22 +216,6 @@ jobs:
echo "No ./target/junit directory; nothing to process."
fi
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
# Related to our custom upload to S3
output-name: e2e-${{ inputs.name }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
# Related to Trunk uploader
variant: e2e-tests
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ inputs.specs && steps.tagged-e2e.outcome || steps.split-e2e.outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Quarantine gate
# Only one of the two run steps executes; the other reports `skipped`. Only gate on actual failure.
if: ${{ !cancelled() && (steps.split-e2e.outcome == 'failure' || steps.tagged-e2e.outcome == 'failure') }}

View File

@@ -111,19 +111,6 @@ jobs:
echo "No ./target/junit directory; nothing to process."
fi
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
output-name: e2e-embedding-sdk-react-${{ matrix.react-version }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-tests.outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Quarantine gate
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
continue-on-error: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
@@ -293,19 +280,6 @@ jobs:
echo "No ./target/junit directory; nothing to process."
fi
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
output-name: e2e-embedding-sdk
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-tests.outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
- name: Quarantine gate
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
continue-on-error: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}

View File

@@ -40,9 +40,9 @@ jobs:
| wc -l || true
)
echo "$(($oss+$ee)) files importing @emotion/*"
echo "$((oss + ee)) files importing @emotion/*"
echo "EMOTION_FILES=$(($oss+$ee))" >> $GITHUB_OUTPUT
echo "EMOTION_FILES=$((oss + ee))" >> "$GITHUB_OUTPUT"
- name: Upload count to Stats
uses: actions/github-script@v9
env:

View File

@@ -1,36 +0,0 @@
name: Escalation Slack Notification
on:
issues:
types:
- labeled
jobs:
on-escalation-label:
if: github.event.label.name == '.Escalation'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
steps:
- name: Format issue title (escape quotes)
uses: actions/github-script@v9
id: vars
with:
script: |
core.setOutput('issue_title', ${{ toJson(github.event.issue.title) }}.replaceAll(/"/g, '\\"'));
- name: Send escalated issue ${{ github.event.issue.number }} to Slack
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_ESCALATIONS_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🔥🔥🔥 ${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
}
}
]
}

View File

@@ -23,15 +23,23 @@ jobs:
timeout-minutes: 5
steps:
- name: Generate workflow summary
env:
BRANCH: ${{ github.ref_name }}
SPEC: ${{ inputs.spec }}
BURN_IN: ${{ inputs.burn_in }}
GREP: ${{ inputs.grep }}
SENDER: ${{ github.event.sender.login }}
run: |
echo '**Inputs:**' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '- `branch`: ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
echo '- `spec`: ${{ inputs.spec }}' >> $GITHUB_STEP_SUMMARY
echo '- `burn_in`: ${{ inputs.burn_in }}' >> $GITHUB_STEP_SUMMARY
echo '- `grep`: "${{ inputs.grep }}"' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
{
echo '**Inputs:**'
echo ''
echo "- \`branch\`: $BRANCH"
echo "- \`spec\`: $SPEC"
echo "- \`burn_in\`: $BURN_IN"
echo "- \`grep\`: \"$GREP\""
echo ''
echo "triggered by: @$SENDER"
} >> "$GITHUB_STEP_SUMMARY"
frontend-unit-test-stress-test-flake-fix:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
@@ -48,10 +56,9 @@ jobs:
- name: Stress-test ${{ github.event.inputs.spec }} ${{ github.event.inputs.burn_in }} times
run: |
for ((i=1; i<=BURN_IN; i++)); do
bun run jest --runInBand --ci --silent --no-cache \
if ! bun run jest --runInBand --ci --silent --no-cache \
--testPathPatterns "${SPEC}" \
--testNamePattern "${GREP}"
if [[ "$?" != 0 ]]; then
--testNamePattern "${GREP}"; then
echo "Failed after $i attempts" && break;
fi
done

View File

@@ -129,18 +129,6 @@ jobs:
adapter: frontend
test-suite: fe-tests-unit
enforce-quarantine: ${{ vars.QUARANTINE_ENGINE == 'ci-conductor' }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always() && github.event_name != 'workflow_dispatch'
with:
input-path: ./target/junit
output-name: frontend-unit
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.fe-tests.outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
fe-tests-timezones:
if: ${{ !inputs.skip }}

View File

@@ -1,58 +0,0 @@
name: Generative Query Test
on:
workflow_dispatch:
inputs:
context-seed:
description: 'Context seed (empty = random)'
type: number
iteration-seed:
description: 'Iteration seed (for iteration replay, empty = dependent on Context seed)'
type: number
iterations:
description: 'Iterations (number of test iterations, overrides seconds)'
type: number
seconds:
description: 'Seconds (Number of seconds after which next iteration will not start)'
test:
description: 'Test or namespace to run'
default: metabase.query-processor.generative-test/execution-with-cards-test
ref:
description: 'Ref to run generative tests on'
default: qgen-ms2
# TODO: Consider renaming the job in next milestones
jobs:
generative-query-test:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 90
name: Generative Query Test
env:
CI: 'true'
MB_GENTEST_CONTEXT_SEED: ${{ inputs.context-seed }}
MB_GENTEST_ITERATION_SEED: ${{ inputs.iteration-seed }}
MB_GENTEST_LIMIT_ITERATIONS: ${{ inputs.iterations }}
MB_GENTEST_LIMIT_SECONDS: ${{ inputs.seconds }}
MB_GENTEST_RUN: true
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Run test
run: clojure -X:dev:ci:test:ee:ee-dev :only '${{ inputs.test }}'
- name: Publish Test Report (JUnit)
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
if: failure()
with:
path: "target/junit/**/*_test.xml"
name: JUnit Test Report (Generative Query test)
reporter: java-junit
list-suites: failed
list-tests: failed
fail-on-error: false

View File

@@ -1,51 +0,0 @@
name: i18n
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
i18n: ${{ steps.changes.outputs.i18n }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
verify-i18n-files:
needs: files-changed
if: needs.files-changed.outputs.i18n == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Install xgettext if necessary
run: |
if ! command -v xgettext > /dev/null; then
sudo apt install gettext
fi
- name: Check xgettext version
run: xgettext --version
- name: Check i18n tags/make sure template can be built
run: ./bin/i18n/update-translation-template
- name: Verify i18n translations (.po files)
run: ./bin/i18n/build-translation-resources

View File

@@ -18,9 +18,9 @@ jobs:
oss=$(find ./frontend/src/metabase -type f -name "*.js" -o -name "*.jsx" | wc -l)
ee=$(find ./enterprise/frontend/src/metabase-enterprise -type f -name "*.js" -o -name "*.jsx" | wc -l)
echo $(($oss+$ee)) javascript files
echo $((oss + ee)) javascript files
echo "JS_FILES=$(($oss+$ee))" >> $GITHUB_OUTPUT
echo "JS_FILES=$((oss + ee))" >> "$GITHUB_OUTPUT"
- name: Upload count to Stats
uses: actions/github-script@v9
env:

184
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,184 @@
name: Lint
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
outputs:
actionlint: ${{ steps.changes.outputs.actionlint }}
actionlint_files: ${{ steps.changes.outputs.actionlint_files }}
codeql: ${{ steps.changes.outputs.codeql }}
i18n: ${{ steps.changes.outputs.i18n }}
openapi: ${{ steps.changes.outputs.openapi }}
snowplow: ${{ steps.changes.outputs.snowplow }}
yaml: ${{ steps.changes.outputs.yaml }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
list-files: shell
lint:
needs: files-changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Setup frontend env
if: |
needs.files-changed.outputs.yaml == 'true' ||
needs.files-changed.outputs.openapi == 'true'
uses: ./.github/actions/prepare-frontend
# Always required by whitespace linter, also used by OpenAPI spec validation
- name: Setup backend env
uses: ./.github/actions/prepare-backend
- name: Setup actionlint
if: needs.files-changed.outputs.actionlint == 'true'
id: download
shell: bash
run: |
if command -v actionlint; then
echo "executable=actionlint" >> "$GITHUB_OUTPUT"
else
# script sets executable output, for details see: https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash)
fi
- parallel:
# Lint whitespace, always runs
- name: Run Whitespace Linter
run: clojure -T:whitespace-linter lint :whitespace-linter/progress-bar false
# Lint YAML
- name: Lint YAML files
if: needs.files-changed.outputs.yaml == 'true'
run: bun run lint-yaml
# Lint Github Actions
- name: Run actionlint
if: needs.files-changed.outputs.actionlint == 'true'
run: ${{ steps.download.outputs.executable }} --color --verbose ${{ needs.files-changed.outputs.actionlint_files }}
# Generate and Lint OpenAPI spec
- name: Generate OpenAPI spec
if: needs.files-changed.outputs.openapi == 'true'
run: bun run generate-openapi
# Parallel steps run asynchronously, and explicitly waiting for a parallel step means "if" statements are ignored.
# Move OpenAPI validation outside the parallel block so that spec generation happens asynchronously without requiring
# an explicit "wait".
- name: Validate OpenAPI spec
if: needs.files-changed.outputs.openapi == 'true'
run: bun run lint-openapi
codeql:
if: needs.files-changed.outputs.codeql == 'true'
needs: files-changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: javascript
config-file: ./.github/codeql/codeql-config.yml
trap-caching: false
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
i18n:
needs: files-changed
if: needs.files-changed.outputs.i18n == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Install xgettext if necessary
run: |
if ! command -v xgettext > /dev/null; then
sudo apt install gettext
fi
- name: Check xgettext version
run: xgettext --version
- name: Check i18n tags/make sure template can be built
run: ./bin/i18n/update-translation-template
- name: Verify i18n translations (.po files)
run: ./bin/i18n/build-translation-resources
snowplow:
needs: files-changed
if: needs.files-changed.outputs.snowplow == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v5.2.0
with:
distribution: temurin
java-version: 25
- name: Set up Babashka
uses: turtlequeue/setup-babashka@d78ec6570aea3b614bc695cafcceb82eb45c2af9 # v1.8.0
with:
babashka-version: 1.12.197
- name: Install igluctl
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/igluctl.zip" https://github.com/snowplow/igluctl/releases/download/0.13.0/igluctl_0.13.0.zip
echo "0eac19121fca1431dec44738ada8eacc4242811a95d015ee71e00deec2b4d748 $RUNNER_TEMP/igluctl.zip" | sha256sum -c -
unzip -q "$RUNNER_TEMP/igluctl.zip" -d "$RUNNER_TEMP/igluctl"
chmod +x "$RUNNER_TEMP/igluctl/igluctl"
echo "$RUNNER_TEMP/igluctl" >> "$GITHUB_PATH"
- name: Lint (igluctl)
# igluctl recursively parses every file as JSON, so it chokes on colocated
# *.md docs and on two schemas whose internal `self.version` doesn't match
# the filename version (pending Snowcat audit). Stage a filtered copy and
# patch only the `self.version` mismatch so igluctl still lints the rest
# of those schemas.
run: |
set -euo pipefail
LINT_ROOT="$RUNNER_TEMP/iglu-lint"
rm -rf "$LINT_ROOT"
mkdir -p "$LINT_ROOT"
cp -r snowplow/iglu-client-embedded/schemas "$LINT_ROOT/"
find "$LINT_ROOT" -name '*.md' -delete
# TODO(BOT-1403): drop these patches once the Snowcat audit fixes the
# internal `self.version` mismatch on these two schemas. We rewrite
# only `self.version` in the staged copy so the file's path version
# matches what igluctl expects; every other lint rule still runs.
# Each `sed` invocation uses the schema's wrong/right pair, so it
# silently no-ops once the underlying schema is fixed.
sed -i 's/"version": "1-0-1"/"version": "1-0-2"/' \
"$LINT_ROOT/schemas/com.metabase/embed_share/jsonschema/1-0-2"
sed -i 's/"version": "1-0-0"/"version": "1-0-1"/' \
"$LINT_ROOT/schemas/com.metabase/serialization/jsonschema/1-0-1"
igluctl lint "$LINT_ROOT/schemas"
- name: Lint (Metabase schema rules)
run: ./bin/lint-snowplow-schemas.bb
- name: Test linter rules
run: ./bin/lint-snowplow-schemas-test.bb

View File

@@ -53,8 +53,8 @@ jobs:
if ! validate_paths "$FILES"; then
exit 1
fi
echo "story_files=$FILES" >> $GITHUB_OUTPUT
echo "should_run=true" >> $GITHUB_OUTPUT
echo "story_files=$FILES" >> "$GITHUB_OUTPUT"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -70,7 +70,7 @@ jobs:
if [[ -z "$FILES" ]]; then
echo "No changed story files found"
echo "should_run=false" >> $GITHUB_OUTPUT
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -79,8 +79,8 @@ jobs:
fi
echo "Changed story files: $FILES"
echo "story_files=$FILES" >> $GITHUB_OUTPUT
echo "should_run=true" >> $GITHUB_OUTPUT
echo "story_files=$FILES" >> "$GITHUB_OUTPUT"
echo "should_run=true" >> "$GITHUB_OUTPUT"
workflow-summary:
name: Summary
@@ -99,22 +99,24 @@ jobs:
PR_ACTOR: ${{ github.event.pull_request.user.login }}
ACTOR: ${{ github.actor }}
run: |
echo '## Loki Stress Test' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo "**Branch:** \`${BRANCH_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '**Story files:**' >> $GITHUB_STEP_SUMMARY
echo "${STORY_FILES}" | tr ',' '\n' | while read -r file; do
echo "- \`$file\`" >> $GITHUB_STEP_SUMMARY
done
echo '' >> $GITHUB_STEP_SUMMARY
echo "**Burn-in:** ${BURN_IN} runs" >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
if [[ "${EVENT_NAME}" == "pull_request" ]]; then
echo "Triggered by PR #${PR_NUMBER} from @${PR_ACTOR}" >> $GITHUB_STEP_SUMMARY
else
echo "Triggered manually by @${ACTOR}" >> $GITHUB_STEP_SUMMARY
fi
{
echo '## Loki Stress Test'
echo ''
echo "**Branch:** \`${BRANCH_NAME}\`"
echo ''
echo '**Story files:**'
echo "${STORY_FILES}" | tr ',' '\n' | while read -r file; do
echo "- \`$file\`"
done
echo ''
echo "**Burn-in:** ${BURN_IN} runs"
echo ''
if [[ "${EVENT_NAME}" == "pull_request" ]]; then
echo "Triggered by PR #${PR_NUMBER} from @${PR_ACTOR}"
else
echo "Triggered manually by @${ACTOR}"
fi
} >> "$GITHUB_STEP_SUMMARY"
stress-test-loki:
needs: detect-changed-stories
@@ -175,8 +177,8 @@ jobs:
echo "❌ Run $i FAILED"
FAILED=$((FAILED + 1))
mkdir -p .loki/failures/run-$i
cp -r .loki/difference/* .loki/failures/run-$i/ 2>/dev/null || true
mkdir -p ".loki/failures/run-$i"
cp -r .loki/difference/* ".loki/failures/run-$i/" 2>/dev/null || true
fi
echo ""
done

View File

@@ -31,9 +31,9 @@ jobs:
id: check-changes
run: |
if [[ -n "$(git status --porcelain .loki/reference/)" ]]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Commit and Push Changes

View File

@@ -1,37 +0,0 @@
name: Slack Notification for New Feature Requests
on:
issues:
types:
- labeled
jobs:
on-issue-labeled:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: |
contains(github.event.issue.labels.*.name, 'Type:New Feature')
steps:
- name: Setting title
uses: actions/github-script@v9
id: vars
with:
script: |
core.setOutput('issue_title', ${{ toJson(github.event.issue.title) }}.replaceAll(/"/g, '\\"'));
- name: Send the issue ${{ github.event.issue.number }} to Slack
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_FEATURE_REQUEST_TRIAGE_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "New incoming feature request: ${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
}
}
]
}

View File

@@ -27,6 +27,6 @@ jobs:
else
echo "metabase.lib.js namespace changed but docs/developers/guide/mbql-library-changelog.md did not"
echo "Update the changelog with the changes made to the library's API."
echo "If the edits did not change the API, add the `.metabase-lib/no-change` label."
echo "If the edits did not change the API, add the \`.metabase-lib/no-change\` label."
exit 1
fi

View File

@@ -9,7 +9,6 @@ concurrency:
cancel-in-progress: true
permissions:
id-token: write
contents: read
checks: write
actions: read
@@ -26,9 +25,9 @@ jobs:
id: check
run: |
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'minimal-tests') }}" == "true" ]]; then
echo "result=true" >> $GITHUB_OUTPUT
echo "result=true" >> "$GITHUB_OUTPUT"
else
echo "result=false" >> $GITHUB_OUTPUT
echo "result=false" >> "$GITHUB_OUTPUT"
fi
# Postgres app-db tests - single version, single job
@@ -64,22 +63,12 @@ jobs:
with:
build-static-viz: true
junit-name: "minimal-be-tests-postgres-ee"
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
test-args: >-
:only '["test" "enterprise/backend/test"]'
:exclude-tags '[:mb/driver-tests :mb/transforms-python-test]'
:partition/total 2
:partition/index 0
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
# Postgres driver tests
postgres-driver-test:
@@ -113,20 +102,10 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: "minimal-be-tests-postgres-driver-ee"
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
# Frontend unit tests
frontend-unit-tests:
@@ -144,21 +123,32 @@ jobs:
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Resolve current job id
# Resolve JOB_ID before the long test step so the ci-conductor reporter
# below can link failures to this exact job. Best-effort.
uses: ./.github/actions/resolve-job-id
- name: Run frontend unit tests
id: run-tests
run: bun run test-unit --silent
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
continue-on-error: true
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
- name: Report failed tests to ci-conductor
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
# This step should NEVER fail the job!
continue-on-error: true
uses: ./.github/actions/report-failures-to-ci-conductor
with:
input-path: ./target/junit
output-name: frontend-unit
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.run-tests.outcome }}
test-suite: fe-tests-unit
adapter: frontend
- name: Quarantine gate
if: ${{ !cancelled() && steps.run-tests.outcome == 'failure' }}
continue-on-error: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
timeout-minutes: 3
uses: ./.github/actions/quarantine-gate
with:
adapter: frontend
test-suite: fe-tests-unit
enforce-quarantine: ${{ vars.QUARANTINE_ENGINE == 'ci-conductor' }}
# Summary job
minimal-tests-result:

View File

@@ -0,0 +1,208 @@
# Send Slack and/or PagerDuty notifications when labels are added to a Github issue.
name: Notify on Issue Labeled
on:
issues:
types:
- labeled
jobs:
on-escalation-label:
if: github.event.label.name == '.Escalation'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- name: Format issue title (escape quotes)
uses: actions/github-script@v9
id: vars
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
with:
script: |
core.setOutput('issue_title', process.env.ISSUE_TITLE.replaceAll(/"/g, '\\"'));
- name: Send escalated issue ${{ github.event.issue.number }} to Slack
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_ESCALATIONS_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🔥🔥🔥 ${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
}
}
]
}
on-security-label:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: github.event.label.name == 'SECURITY'
steps:
- name: Setting title
uses: actions/github-script@v9
id: vars
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
with:
script: |
core.setOutput('issue_title', process.env.ISSUE_TITLE.replaceAll(/"/g, '\\"'));
- name: Send the issue ${{ github.event.issue.number }} to Slack
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_SECURITY_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
}
}
]
}
# Note: this only triggers if a issue is already labeled with `Type:Bug` and `flaky-test-fix` and then a team label is added.
on-team-label:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: |
contains(github.event.label.name, '.Team/') &&
( contains(github.event.issue.labels.*.name, 'Type:Bug') ||
contains(github.event.issue.labels.*.name, 'flaky-test-fix') )
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
- name: Build Slack message (with Linear link if available)
uses: actions/github-script@v9
id: vars
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
with:
script: |
const { findLinearIssue } = require('./.github/scripts/find-linear-issue.js');
const title = context.payload.issue.title;
const issueUrl = context.payload.issue.html_url;
let text = title + '\n' + issueUrl;
// The Linear issue is usually already synced by the time the
// team label lands at triage, but retry briefly just in case.
try {
const delaysSeconds = [0, 15, 30];
let result = null;
for (const delay of delaysSeconds) {
if (delay) await new Promise((r) => setTimeout(r, delay * 1000));
result = await findLinearIssue({
githubIssueUrl: issueUrl,
linearApiKey: process.env.LINEAR_API_KEY,
});
if (result.success) break;
}
if (result && result.success) {
const id = result.issue; // e.g. "GIT-1234"
text += `\nLinear: <https://linear.app/metabase/issue/${id}|${id}>`;
} else {
core.info(`No Linear issue found for ${issueUrl}; posting without link.`);
}
} catch (e) {
// Never block the notification on a Linear lookup failure
core.warning(`Linear lookup failed: ${e.message}`);
}
core.setOutput('message', JSON.stringify(text));
- name: Extracting team name
id: team_name
env:
LABEL_NAME: ${{ github.event.label.name }}
run: |
team_label="${LABEL_NAME//&/$}"
echo "$team_label"
team_name=${team_label#*.Team/}
echo "$team_name"
echo "team_name=${team_name^^}_SLACK_ISSUES_WEBHOOK_URL" >> "$GITHUB_OUTPUT"
- name: Check webhook secret exists
id: check_secret
env:
WEBHOOK_URL: ${{ secrets[steps.team_name.outputs.team_name] }}
run: |
if [ -z "$WEBHOOK_URL" ]; then
echo "Secret ${{ steps.team_name.outputs.team_name }} is not set, skipping Slack notification"
echo "exists=false" >> "$GITHUB_OUTPUT"
else
echo "exists=true" >> "$GITHUB_OUTPUT"
fi
- name: Send the issue ${{ github.event.issue.number }} to Slack
if: steps.check_secret.outputs.exists == 'true'
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets[steps.team_name.outputs.team_name] }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ${{ steps.vars.outputs.message }}
}
}
]
}
on-escalation-and-team-querying-label:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: |
(github.event.label.name == 'Priority:P0' || github.event.label.name == 'Priority:P1' || github.event.label.name == '.Escalation') &&
contains(github.event.issue.labels.*.name, '.Team/Querying')
steps:
- name: Trigger PagerDuty Incident
env:
ROUTING_KEY: ${{ secrets.QUERYING_PAGERDUTY_INTEGRATION_KEY }}
ISSUE_TITLE: ${{ github.event.issue.title }}
LABEL_NAME: ${{ github.event.label.name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_URL: ${{ github.event.issue.html_url }}
REPO: ${{ github.repository }}
ALL_LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }}
run: |
payload=$(jq -n \
--arg rk "$ROUTING_KEY" \
--arg dk "github-issue-$ISSUE_NUMBER" \
--arg sum "[$LABEL_NAME] $ISSUE_TITLE" \
--arg src "GitHub Issues - $REPO" \
--arg lbl "$ALL_LABELS" \
--arg url "$ISSUE_URL" \
'{
routing_key: $rk,
event_action: "trigger",
dedup_key: $dk,
payload: {
summary: $sum,
source: $src,
severity: "critical",
custom_details: { Labels: $lbl }
},
links: [{ href: $url, text: "View GitHub Issue" }]
}')
curl -X POST https://events.pagerduty.com/v2/enqueue \
-H 'Content-Type: application/json' \
-d "$payload"

View File

@@ -31,7 +31,7 @@ jobs:
- name: Get latest base branch commit
run: |
git fetch origin ${{ github.event.pull_request.base.ref }}
echo "LATEST_BASE_SHA=$(git rev-parse origin/${{ github.event.pull_request.base.ref }})" >> $GITHUB_ENV
echo "LATEST_BASE_SHA=$(git rev-parse origin/${{ github.event.pull_request.base.ref }})" >> "$GITHUB_ENV"
- name: Check OpenAPI specs are up to date
env:
AUTO_COMMIT: "true"

View File

@@ -1,44 +0,0 @@
name: Validate OpenAPI
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
openapi: ${{ steps.changes.outputs.openapi }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
validate-openapi:
needs: files-changed
if: needs.files-changed.outputs.openapi == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Generate OpenAPI spec
run: bun run generate-openapi
- name: Validate OpenAPI spec
run: bun run lint-openapi

View File

@@ -63,10 +63,12 @@ jobs:
total_saved=$((total_before - total_after))
# Output results for use in commit message
echo "files_optimized=${total_files}" >> "$GITHUB_OUTPUT"
echo "bytes_saved=${total_saved}" >> "$GITHUB_OUTPUT"
echo "total_before=${total_before}" >> "$GITHUB_OUTPUT"
echo "total_after=${total_after}" >> "$GITHUB_OUTPUT"
{
echo "files_optimized=${total_files}"
echo "bytes_saved=${total_saved}"
echo "total_before=${total_before}"
echo "total_after=${total_after}"
} >> "$GITHUB_OUTPUT"
echo "Optimization complete"
echo "Files processed: ${total_files}"

View File

@@ -70,7 +70,7 @@ jobs:
export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${PR_ENV_K8S_AUDIENCE}"
IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" | jq -r .value)
echo "::add-mask::${IDTOKEN}"
echo "idToken=${IDTOKEN}" >>$GITHUB_OUTPUT
echo "idToken=${IDTOKEN}" >>"$GITHUB_OUTPUT"
env:
PR_ENV_K8S_AUDIENCE: ${{ secrets.PR_ENV_K8S_AUDIENCE }}
- name: Setup Kube Context
@@ -108,7 +108,7 @@ jobs:
env:
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
id: split
run: echo "fragment=${SHA:5}" >> $GITHUB_OUTPUT
run: echo "fragment=${SHA:5}" >> "$GITHUB_OUTPUT"
- name: Render Deployment YAML
uses: nowactions/envsubst@c4b8e3977354d294659a7ec164279ce8fba5c2e1 # v1.0.1
with:

View File

@@ -18,7 +18,10 @@ jobs:
environment: pr-env-admin
steps:
- name: Approved
run: echo "Destroying PR environment for PR #${{ inputs.pr_number }}"
env:
PR_NUMBER: ${{ inputs.pr_number }}
run: |
echo "Destroying PR environment for PR #${PR_NUMBER}"
destroy:
needs: approve

View File

@@ -42,7 +42,7 @@ jobs:
export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${PR_ENV_K8S_AUDIENCE}"
IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" | jq -r .value)
echo "::add-mask::${IDTOKEN}"
echo "idToken=${IDTOKEN}" >>$GITHUB_OUTPUT
echo "idToken=${IDTOKEN}" >>"$GITHUB_OUTPUT"
env:
PR_ENV_K8S_AUDIENCE: ${{ secrets.PR_ENV_K8S_AUDIENCE }}
- name: Setup Kube Context

View File

@@ -249,7 +249,7 @@ jobs:
export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${PR_ENV_K8S_AUDIENCE}"
IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" | jq -r .value)
echo "::add-mask::${IDTOKEN}"
echo "idToken=${IDTOKEN}" >>$GITHUB_OUTPUT
echo "idToken=${IDTOKEN}" >>"$GITHUB_OUTPUT"
env:
PR_ENV_K8S_AUDIENCE: ${{ secrets.PR_ENV_K8S_AUDIENCE }}
- name: Setup Kube Context

View File

@@ -95,14 +95,14 @@ jobs:
id: version-properties
run: |
cat version.properties
echo "commit=$(cat ./COMMIT-ID)" >> $GITHUB_OUTPUT
echo "commit=$(cat ./COMMIT-ID)" >> "$GITHUB_OUTPUT"
version=$(grep -o '^tag=.*' version.properties | cut -d'=' -f2)
if [[ "${{ matrix.edition }}" == "ee" ]]; then
echo "ee_version=$version" >> $GITHUB_OUTPUT
echo "ee_version=$version" >> "$GITHUB_OUTPUT"
else
echo "oss_version=$version" >> $GITHUB_OUTPUT
echo "oss_version=$version" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Upload Metabase ${{ matrix.edition }} JAR as artifact
@@ -223,7 +223,7 @@ jobs:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: |
node e2e/runner/run_cypress_ci.js e2e \
$CYPRESS_GREP_FLAG grepTags="@prerelease+-@EE" \
"$CYPRESS_GREP_FLAG" grepTags="@prerelease+-@EE" \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)'
- name: Run a few important EE Cypress tests as sanity check
@@ -232,7 +232,7 @@ jobs:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: |
node e2e/runner/run_cypress_ci.js e2e \
$CYPRESS_GREP_FLAG grepTags="@prerelease+-@OSS" \
"$CYPRESS_GREP_FLAG" grepTags="@prerelease+-@OSS" \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)'
- name: Upload Cypress Artifacts upon failure
@@ -254,7 +254,7 @@ jobs:
commit: ${{ inputs.commit }}
repo: ${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}
tag: ${{ needs.release-artifact.outputs.oss_version }}-${{ inputs.commit }}
release: "true"
release: true
containerize-ee:
needs: release-artifact
@@ -265,7 +265,7 @@ jobs:
commit: ${{ inputs.commit }}
repo: ${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}
tag: ${{ needs.release-artifact.outputs.ee_version }}-${{ inputs.commit }}
release: "true"
release: true
tests-complete-message:
if: always()

View File

@@ -1,109 +0,0 @@
# this file uses MBA https://github.com/metabase/mba to spin up a Metabase
# instance then runs the test script in the MBA instance sharing the same
# network to interact with presto container
# TODO: get rid of MBA and use actions/prepare-backend instead
name: Kerberized Presto Integration Test
on:
pull_request:
push:
branches:
- master
- "release**"
- "feature**"
tags:
- "**"
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
backend_presto_kerberos: ${{ steps.changes.outputs.backend_presto_kerberos }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
run-presto-kerberos-test:
needs: files-changed
if: needs.files-changed.outputs.backend_presto_kerberos == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
steps:
- name: Install babashka
run: >
mkdir -p /tmp/babashka-install \
&& cd /tmp/babashka-install \
&& curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install \
&& chmod +x install \
&& sudo ./install \
&& cd -
- name: Checkout Metabase repository
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check out Presto Kerberos Docker Compose
uses: actions/checkout@v6
with:
repository: metabase/presto-kerberos-docker
ref: master
token: ${{ secrets.GITHUB_TOKEN }}
path: presto-kerberos-docker
- name: Bring up Presto+Kerberos cluster
run: cd presto-kerberos-docker && docker compose up -d && cd ..
- name: Run Presto test query from command line (sanity check)
run: cd presto-kerberos-docker && ./test.sh && cd ..
# Since we are managing the Docker containers from the GitHub action container, we need to copy all the
# relevant resources now, into the resources dir for later consumption by the app
- name: Copy Presto SSL keystore to resources
run: docker cp presto-kerberos:/tmp/ssl_keystore.jks resources
- name: Copy krb5.conf file to resources
run: docker cp presto-kerberos:/etc/krb5.conf resources
- name: Copy client.keytab file to resources
run: docker cp presto-kerberos:/home/presto/client.keytab resources
- name: Checkout mba
uses: actions/checkout@v6
with:
repository: metabase/mba
ref: master
token: ${{ secrets.GITHUB_TOKEN }}
path: mba-src
- name: ls mba
run: ls -latr mba-src
- name: Symlink mba
run: cd mba-src && sudo ln -s $(pwd)/src/main.clj /usr/local/bin/mba && chmod +x /usr/local/bin/mba && cd ..
- name: Ensure mba
run: which mba
- name: Run Metabase via MBA
run: mba --mb . --data-db postgres-data -n example.com compose config > mba-compose.yml
- name: Adjust generated docker-compose config
uses: actions/github-script@v9
with:
script: | #js
const fs = require("fs");
const config = fs.readFileSync("mba-compose.yml", "utf8");
const newConfig = config.replace(
/name:\s+example.com/,
"name: example.com\n external: true",
);
fs.writeFileSync("mba-compose.yml", newConfig);
- name: Print compose config
run: cat mba-compose.yml
- name: Run mba
run: docker compose -f mba-compose.yml up -d
- name: Run test script in MBA instance
run: docker compose -f mba-compose.yml exec metabase .github/scripts/run-presto-kerberos-integration-test.sh

View File

@@ -1,47 +0,0 @@
name: Querying On-call PagerDuty Alert
on:
issues:
types:
- labeled
jobs:
notify-querying-on-call:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: |
(github.event.label.name == 'Priority:P0' || github.event.label.name == 'Priority:P1' || github.event.label.name == '.Escalation') &&
contains(github.event.issue.labels.*.name, '.Team/Querying')
steps:
- name: Trigger PagerDuty Incident
env:
ROUTING_KEY: ${{ secrets.QUERYING_PAGERDUTY_INTEGRATION_KEY }}
ISSUE_TITLE: ${{ github.event.issue.title }}
LABEL_NAME: ${{ github.event.label.name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_URL: ${{ github.event.issue.html_url }}
REPO: ${{ github.repository }}
ALL_LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }}
run: |
payload=$(jq -n \
--arg rk "$ROUTING_KEY" \
--arg dk "github-issue-$ISSUE_NUMBER" \
--arg sum "[$LABEL_NAME] $ISSUE_TITLE" \
--arg src "GitHub Issues - $REPO" \
--arg lbl "$ALL_LABELS" \
--arg url "$ISSUE_URL" \
'{
routing_key: $rk,
event_action: "trigger",
dedup_key: $dk,
payload: {
summary: $sum,
source: $src,
severity: "critical",
custom_details: { Labels: $lbl }
},
links: [{ href: $url, text: "View GitHub Issue" }]
}')
curl -X POST https://events.pagerduty.com/v2/enqueue \
-H 'Content-Type: application/json' \
-d "$payload"

View File

@@ -17,14 +17,11 @@ on:
workflow_dispatch:
inputs:
read_first:
description: |
⚠️ To cut a release, use the "SDK Version Bump PR" workflow instead — it bumps the version and opens a PR; merging that PR publishes.
This publishes the version already committed in package.template.json. It never bumps. Pick the branch in "Use workflow from" above.
Use it only when neither the bump PR nor re-running the failed jobs will do.
description: "⚠️ Manual override. Publishes whatever version is already committed in package.template.json; it never bumps. Normally you want the SDK Version Bump PR workflow, where merging the PR publishes."
type: choice
required: false
options:
- Publish the committed version from the branch picked in "Use workflow from" above
- Publishes the branch picked above
# Step 2 of the new release process: merging a version-bump PR (opened by
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) triggers a release
# of whatever branch that PR targeted. See `prepare-release` below.

View File

@@ -71,12 +71,12 @@ jobs:
- name: generate release Log
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cd release && tsx ./src/release-log-run.ts $VERSION > v$VERSION.html
run: cd release && tsx ./src/release-log-run.ts "$VERSION" > "v$VERSION.html"
- name: upload release log to the web
run: |
aws s3 cp \
release/v$VERSION.html \
s3://${{ vars.AWS_S3_STATIC_BUCKET }}/release-log/v$VERSION.html
"release/v$VERSION.html" \
"s3://${{ vars.AWS_S3_STATIC_BUCKET }}/release-log/v$VERSION.html"
- name: Create cloudfront invalidation
run: |

View File

@@ -238,12 +238,12 @@ jobs:
run: | # sh
if [[ "${{ matrix.edition }}" == "ee" ]]; then
echo "Metabase EE: image is going to be pushed to ${{ github.repository_owner }}/metabase-enterprise"
echo "DOCKERHUB_REPO=${{ github.repository_owner }}/metabase-enterprise" >> $GITHUB_ENV
echo "DOCKERHUB_VERSION=${{ needs.check-version.outputs.ee }}" >> $GITHUB_ENV
echo "DOCKERHUB_REPO=${{ github.repository_owner }}/metabase-enterprise" >> "$GITHUB_ENV"
echo "DOCKERHUB_VERSION=${{ needs.check-version.outputs.ee }}" >> "$GITHUB_ENV"
else
echo "Metabase OSS: image is going to be pushed to ${{ github.repository_owner }}/metabase"
echo "DOCKERHUB_REPO=${{ github.repository_owner }}/metabase" >> $GITHUB_ENV
echo "DOCKERHUB_VERSION=${{ needs.check-version.outputs.oss }}" >> $GITHUB_ENV
echo "DOCKERHUB_REPO=${{ github.repository_owner }}/metabase" >> "$GITHUB_ENV"
echo "DOCKERHUB_VERSION=${{ needs.check-version.outputs.oss }}" >> "$GITHUB_ENV"
fi
- name: Login to Docker Hub
@@ -255,8 +255,8 @@ jobs:
- name: Tag the docker image
run: | # sh
docker buildx imagetools create --tag \
$DOCKERHUB_REPO\:$TAG_NAME \
$DOCKERHUB_REPO\:$DOCKERHUB_VERSION
"$DOCKERHUB_REPO:$TAG_NAME" \
"$DOCKERHUB_REPO:$DOCKERHUB_VERSION"
push-git-tags:
permissions:

View File

@@ -281,7 +281,7 @@ jobs:
commit: ${{ inputs.commit }}
repo: ${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_REPO }}
tag: ${{ needs.check-version.outputs.oss }}
release: ${{ 'true' }}
release: true
containerize-ee:
needs: check-version
@@ -292,7 +292,7 @@ jobs:
commit: ${{ inputs.commit }}
repo: ${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_REPO }}-enterprise
tag: ${{ needs.check-version.outputs.ee }}
release: ${{ 'true' }}
release: true
push-tags:
needs: [verify-s3-download, containerize-oss, containerize-ee, check-version]
@@ -381,7 +381,8 @@ jobs:
dot-x-tag: ${{ startsWith(matrix.tag, 'v') }}
trigger-cloud-issues:
if: ${{ needs.check-version.outputs.kind == 'minor' || needs.check-version.outputs.kind == 'major' }}
# Only create issues in the cloud ops repository if this release happens in https://github.com/metabase/metabase.
if: ${{ github.repository == 'metabase/metabase' && (needs.check-version.outputs.kind == 'minor' || needs.check-version.outputs.kind == 'major') }}
needs: [push-tags, check-version]
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10

View File

@@ -37,7 +37,7 @@ jobs:
else
ISSUE_NUMBER="${{ github.event.issue.number }}"
fi
echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
echo "number=$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"
echo "Issue: https://github.com/${{ github.repository }}/issues/$ISSUE_NUMBER"
env:
@@ -58,7 +58,7 @@ jobs:
run: git clone --bare "https://github.com/${{ github.repository }}.git" repro-bot/git/metabase
- name: Capture Node.js path for MCP subprocesses
run: echo "NODE_BIN_DIR=$(dirname $(which node))" >> $GITHUB_ENV
run: echo "NODE_BIN_DIR=$(dirname "$(which node)")" >> "$GITHUB_ENV"
- name: Pre-install MCP server dependencies
run: uv sync --directory repro-bot/mcp
@@ -90,7 +90,7 @@ jobs:
--main-opts '["-m" "clojure-mcp-light.nrepl-eval"]'
# Add to PATH for the rest of the workflow
echo "$HOME/.bbin/bin" >> $GITHUB_PATH
echo "$HOME/.bbin/bin" >> "$GITHUB_PATH"
# Verify
clj-nrepl-eval --help | head -3

View File

@@ -63,9 +63,9 @@ jobs:
id: pkg
run: |
if [ -f "bun.lock" ] || [ -f "bun.lockb" ]; then
echo "mgr=bun" >> $GITHUB_OUTPUT
echo "mgr=bun" >> "$GITHUB_OUTPUT"
else
echo "mgr=yarn" >> $GITHUB_OUTPUT
echo "mgr=yarn" >> "$GITHUB_OUTPUT"
fi
- name: Validate backport PR
@@ -92,13 +92,15 @@ jobs:
# Check if we're in a cherry-pick state with conflicts
CONFLICTED=$(git diff --name-only --diff-filter=U || true)
if [ -n "$CONFLICTED" ]; then
echo "has_conflicts=true" >> $GITHUB_OUTPUT
echo "conflicted_files<<EOF" >> $GITHUB_OUTPUT
echo "$CONFLICTED" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
{
echo "has_conflicts=true"
echo "conflicted_files<<EOF"
echo "$CONFLICTED"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "::notice::Found conflicts in: $CONFLICTED"
else
echo "has_conflicts=false" >> $GITHUB_OUTPUT
echo "has_conflicts=false" >> "$GITHUB_OUTPUT"
echo "::notice::Cherry-pick applied cleanly, no conflicts to resolve"
fi
@@ -208,23 +210,25 @@ jobs:
if [ -f "${{ steps.claude.outputs.execution_file }}" ]; then
SUBTYPE=$(jq -r '.[] | select(.type == "result") | .subtype' "${{ steps.claude.outputs.execution_file }}" | tail -1)
if [ "$SUBTYPE" = "error_max_turns" ]; then
echo "max_turns_exceeded=true" >> $GITHUB_OUTPUT
echo "max_turns_exceeded=true" >> "$GITHUB_OUTPUT"
else
echo "max_turns_exceeded=false" >> $GITHUB_OUTPUT
echo "max_turns_exceeded=false" >> "$GITHUB_OUTPUT"
fi
else
echo "max_turns_exceeded=false" >> $GITHUB_OUTPUT
echo "max_turns_exceeded=false" >> "$GITHUB_OUTPUT"
fi
- name: Get commit details
if: always() && steps.claude.outcome == 'success' && steps.check-failure.outputs.max_turns_exceeded != 'true'
id: commit-info
run: |
echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
FILES=$(git diff --name-only HEAD~1)
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
echo "$FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
{
echo "sha=$(git rev-parse HEAD)"
echo "changed_files<<EOF"
echo "$FILES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Comment on PR (success)
if: always() && steps.claude.outcome == 'success' && steps.check-failure.outputs.max_turns_exceeded != 'true'

View File

@@ -3,6 +3,19 @@
# package.template.json, then open a PR labeled `sdk-release-trigger`.
# Merging that PR is what actually triggers a release, via the
# `pull_request` trigger on release-embedding-sdk.yml.
#
# Release types (the dispatch form lists them in release-cycle order):
# beta - release branch, before gold: publish a beta.
# patch - release branch: graduate the beta to 0.N.0 at gold,
# then ship 0.N.1, 0.N.2 and so on after gold.
# preminor - master, at gold: move master to the next major.
# alpha - master, any time: bump the development version.
# custom - one-off branch (esbuild, data-apps): publish under its
# own prerelease id, which is reused on later bumps.
#
# The dispatch form's descriptions render as the input's label, which is a
# single plain text node - newlines collapse to spaces and no markdown is
# parsed. Keep them short and single-line; put detail here instead.
name: SDK Version Bump PR
run-name: Bump Embedding SDK version (${{ inputs.release_type }}) on ${{ github.ref_name }}
@@ -14,32 +27,26 @@ on:
type: choice
required: false
options:
- Pick the branch in "Use workflow from" above
- Bumps the branch picked above
release_type:
description: |
Before gold:
• beta — release branch: publish a beta
At gold:
• patch — release branch: graduate the beta to 0.N.0
• preminor — master: move master to the next major
After gold:
• patch — release branch: ship a patch (0.N.1, 0.N.2, ...)
Any time:
• alpha — master: bump the development version
• custom — one-off branch (esbuild, data-apps): publish under its own prerelease id
# Option names are capitalised for emphasis only - the values stay
# lowercase. The label renders as plain text, so caps is the only
# emphasis available (same reason prerelease_id below shouts FIRST).
description: "BETA - before gold, on a release branch. PATCH - at gold and after, on a release branch. PREMINOR - at gold, on master. ALPHA - any time, on master. CUSTOM - any time, on a one-off branch."
type: choice
required: true
# Pinned so the release-cycle ordering below does not decide the
# preselection: without a default, GitHub preselects the first option.
default: alpha
options:
- alpha
- beta
- patch # one entry, two phases: the version decides 0.N.0 vs 0.N.1
- preminor
- patch
- alpha
- custom
prerelease_id:
description: "release_type=custom only, and only on the FIRST cut of a one-off branch (e.g. data-apps). Becomes the prerelease id in the version (0.62.5-data-apps.0) and the npm dist-tag (62-data-apps). Leave empty on later bumps - the id is reused from the current version."
# Left empty on later bumps, the id is reused from the current version.
description: "For release_type=custom only, and only on the FIRST cut of a one-off branch. Enter a name like data-apps: the version becomes 0.62.5-data-apps.0 and the dist-tag 62-data-apps. Leave empty on later bumps."
type: string
required: false
@@ -155,7 +162,10 @@ jobs:
# --reviewer tags the team so a release is visible, not because anything
# waits on a review. It needs the automation PAT: GITHUB_TOKEN cannot
# resolve an org team.
gh pr create --base "$BRANCH" \
#
# gh prints the new PR's URL on stdout; capture it so the job summary
# can link the PR that this run opened.
pr_url=$(gh pr create --base "$BRANCH" \
--assignee "${GITHUB_ACTOR}" \
--reviewer metabase/embedding \
--label sdk-release-trigger \
@@ -163,7 +173,7 @@ jobs:
--body "$(cat <<'BODY'
Bumps the Embedding SDK version in `package.template.json` and updates its `sdkRelease` metadata.
| | |
| Setting | Value |
|---|---|
| Branch | `${{ github.ref_name }}` |
| Release type | `${{ inputs.release_type }}` |
@@ -177,4 +187,14 @@ jobs:
_Opened by the [SDK Version Bump PR workflow](../actions/workflows/sdk-version-bump-pr.yml), triggered by @${{ github.event.sender.login }}._
BODY
)"
)")
echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
# Separate from "Append workflow summary" above: that step runs before the
# PR exists, so the link has to be appended after this one opens it.
- name: Append the PR link to the workflow summary
env:
PR_URL: ${{ steps.open-pr.outputs.pr_url }}
run: |
printf '\n**Version-bump PR:** %s\n' "$PR_URL" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,37 +0,0 @@
name: Security Issue Slack Notification
on:
issues:
types:
- labeled
jobs:
on-issue-opened:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: github.event.label.name == 'SECURITY'
steps:
- name: Setting title
uses: actions/github-script@v9
id: vars
with:
script: |
core.setOutput('issue_title', ${{ toJson(github.event.issue.title) }}.replaceAll(/"/g, '\\"'));
- name: Send the issue ${{ github.event.issue.number }} to Slack
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_SECURITY_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
}
}
]
}

View File

@@ -58,22 +58,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
semantic-search-tests-mariadb:
if: ${{ !inputs.skip }}
@@ -109,6 +98,8 @@ jobs:
POSTGRES_PASSWORD: postgres
mysql:
image: ${{ matrix.version.mariadb-image }}
# disposable CI database: a RAM-backed datadir makes DDL/insert fsyncs free
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -133,22 +124,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
semantic-search-tests-mysql:
if: ${{ !inputs.skip }}
@@ -191,6 +171,8 @@ jobs:
POSTGRES_PASSWORD: postgres
mysql:
image: ${{ matrix.version.mysql-image }}
# disposable CI database: a RAM-backed datadir makes DDL/insert fsyncs free
options: --tmpfs /var/lib/mysql:rw
ports:
- "3306:3306"
env:
@@ -215,22 +197,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
semantic-search-tests-postgres:
if: ${{ !inputs.skip }}
@@ -291,22 +262,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
# 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
@@ -357,22 +317,11 @@ jobs:
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
quarantine-engine: ${{ vars.QUARANTINE_ENGINE }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
semantic-search-tests-result:
needs:

View File

@@ -15,6 +15,8 @@ on:
jobs:
set-milestone:
name: Automatically set milestone
# Only run set-milestone in https://github.com/metabase/metabase since this is the only place milestones are used.
if: github.repository == 'metabase/metabase'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 30
permissions:

View File

@@ -1,75 +0,0 @@
name: Snowplow
on:
pull_request:
branches:
- '**'
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
snowplow: ${{ steps.changes.outputs.snowplow }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
lint:
needs: files-changed
if: needs.files-changed.outputs.snowplow == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v5.2.0
with:
distribution: temurin
java-version: 25
- name: Set up Babashka
uses: turtlequeue/setup-babashka@d78ec6570aea3b614bc695cafcceb82eb45c2af9 # v1.8.0
with:
babashka-version: 1.12.197
- name: Install igluctl
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/igluctl.zip" https://github.com/snowplow/igluctl/releases/download/0.13.0/igluctl_0.13.0.zip
echo "0eac19121fca1431dec44738ada8eacc4242811a95d015ee71e00deec2b4d748 $RUNNER_TEMP/igluctl.zip" | sha256sum -c -
unzip -q "$RUNNER_TEMP/igluctl.zip" -d "$RUNNER_TEMP/igluctl"
chmod +x "$RUNNER_TEMP/igluctl/igluctl"
echo "$RUNNER_TEMP/igluctl" >> "$GITHUB_PATH"
- name: Lint (igluctl)
# igluctl recursively parses every file as JSON, so it chokes on colocated
# *.md docs and on two schemas whose internal `self.version` doesn't match
# the filename version (pending Snowcat audit). Stage a filtered copy and
# patch only the `self.version` mismatch so igluctl still lints the rest
# of those schemas.
run: |
set -euo pipefail
LINT_ROOT="$RUNNER_TEMP/iglu-lint"
rm -rf "$LINT_ROOT"
mkdir -p "$LINT_ROOT"
cp -r snowplow/iglu-client-embedded/schemas "$LINT_ROOT/"
find "$LINT_ROOT" -name '*.md' -delete
# TODO(BOT-1403): drop these patches once the Snowcat audit fixes the
# internal `self.version` mismatch on these two schemas. We rewrite
# only `self.version` in the staged copy so the file's path version
# matches what igluctl expects; every other lint rule still runs.
# Each `sed` invocation uses the schema's wrong/right pair, so it
# silently no-ops once the underlying schema is fixed.
sed -i 's/"version": "1-0-1"/"version": "1-0-2"/' \
"$LINT_ROOT/schemas/com.metabase/embed_share/jsonschema/1-0-2"
sed -i 's/"version": "1-0-0"/"version": "1-0-1"/' \
"$LINT_ROOT/schemas/com.metabase/serialization/jsonschema/1-0-1"
igluctl lint "$LINT_ROOT/schemas"
- name: Lint (Metabase schema rules)
run: ./bin/lint-snowplow-schemas.bb
- name: Test linter rules
run: ./bin/lint-snowplow-schemas-test.bb

View File

@@ -10,6 +10,7 @@ on:
- "**/package.json"
- ".github/workflows/snyk.yml"
- ".github/scripts/write-poms.sh"
- ".github/scripts/fix-yarn-lock-aliases.ts"
schedule:
- cron: "0 5 * * *"
@@ -58,18 +59,22 @@ jobs:
# 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. They continue on error to let
# Snyk report parsing issues directly rather than failing on the workaround.
# 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

View File

@@ -24,18 +24,25 @@ jobs:
timeout-minutes: 5
steps:
- name: Generate workflow summary
env:
RELEASE_BRANCH_COUNT: ${{ inputs.release_branch_count || 5 }}
FORCE: ${{ inputs.force || false }}
EVENT_NAME: ${{ github.event_name }}
SENDER: ${{ github.event.sender.login }}
run: |
echo '**Inputs:**' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '- `release_branch_count`: ${{ inputs.release_branch_count || 5 }}' >> $GITHUB_STEP_SUMMARY
echo '- `force`: ${{ inputs.force || false }}' >> $GITHUB_STEP_SUMMARY
echo '- `trigger`: ${{ github.event_name }}' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
else
echo 'triggered by: schedule' >> $GITHUB_STEP_SUMMARY
fi
{
echo '**Inputs:**'
echo ''
echo "- \`release_branch_count\`: $RELEASE_BRANCH_COUNT"
echo "- \`force\`: $FORCE"
echo "- \`trigger\`: $EVENT_NAME"
echo ''
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "triggered by: @$SENDER"
else
echo 'triggered by: schedule'
fi
} >> "$GITHUB_STEP_SUMMARY"
sync-master:
# ONLY run on internal metabase org forks — NEVER on the public repo or external forks
@@ -89,9 +96,9 @@ jobs:
REPO: ${{ github.repository }}
run: |
if [ "${{ steps.sync.outcome }}" = "success" ]; then
echo "| [\`master\`](https://github.com/${REPO}/tree/master) | ✅ ${{ steps.sync.outputs.detail }} |" >> $GITHUB_STEP_SUMMARY
echo "| [\`master\`](https://github.com/${REPO}/tree/master) | ✅ ${{ steps.sync.outputs.detail }} |" >> "$GITHUB_STEP_SUMMARY"
else
echo "| [\`master\`](https://github.com/${REPO}/tree/master) | ❌ failed |" >> $GITHUB_STEP_SUMMARY
echo "| [\`master\`](https://github.com/${REPO}/tree/master) | ❌ failed |" >> "$GITHUB_STEP_SUMMARY"
fi
list-release-branches:
@@ -190,7 +197,7 @@ jobs:
run: |
branch="${{ matrix.branch }}"
if [ "${{ steps.sync.outcome }}" = "success" ]; then
echo "| [\`${branch}\`](https://github.com/${REPO}/tree/${branch}) | ✅ ${{ steps.sync.outputs.detail }} |" >> $GITHUB_STEP_SUMMARY
echo "| [\`${branch}\`](https://github.com/${REPO}/tree/${branch}) | ✅ ${{ steps.sync.outputs.detail }} |" >> "$GITHUB_STEP_SUMMARY"
else
echo "| [\`${branch}\`](https://github.com/${REPO}/tree/${branch}) | ❌ failed |" >> $GITHUB_STEP_SUMMARY
echo "| [\`${branch}\`](https://github.com/${REPO}/tree/${branch}) | ❌ failed |" >> "$GITHUB_STEP_SUMMARY"
fi

View File

@@ -1,114 +0,0 @@
# .github/workflows/team-issues-slack-notification.yml
#
# Posts newly team-labeled bugs to the team's Slack channel (e.g.
# #team-graphy-bugs), now including a link to the synced Linear issue.
#
# Changes from the current version:
# - Checks out .github/scripts so we can reuse find-linear-issue.js
# - The message-building step now queries Linear (via attachmentsForURL)
# with a short retry, and appends "Linear: <link|ID>" when found
# - Degrades gracefully: if no Linear issue is attached (or the API is
# down), the message posts without the Linear issue link
name: Team Issues Slack Notification
on:
issues:
types:
- labeled
jobs:
on-team-label-added:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
if: |
contains(github.event.label.name, '.Team/') &&
( contains(github.event.issue.labels.*.name, 'Type:Bug') ||
contains(github.event.issue.labels.*.name, 'flaky-test-fix') )
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
- name: Build Slack message (with Linear link if available)
uses: actions/github-script@v9
id: vars
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
with:
script: |
const { findLinearIssue } = require('./.github/scripts/find-linear-issue.js');
const title = context.payload.issue.title;
const issueUrl = context.payload.issue.html_url;
let text = title + '\n' + issueUrl;
// The Linear issue is usually already synced by the time the
// team label lands at triage, but retry briefly just in case.
try {
const delaysSeconds = [0, 15, 30];
let result = null;
for (const delay of delaysSeconds) {
if (delay) await new Promise((r) => setTimeout(r, delay * 1000));
result = await findLinearIssue({
githubIssueUrl: issueUrl,
linearApiKey: process.env.LINEAR_API_KEY,
});
if (result.success) break;
}
if (result && result.success) {
const id = result.issue; // e.g. "GIT-1234"
text += `\nLinear: <https://linear.app/metabase/issue/${id}|${id}>`;
} else {
core.info(`No Linear issue found for ${issueUrl}; posting without link.`);
}
} catch (e) {
// Never block the notification on a Linear lookup failure
core.warning(`Linear lookup failed: ${e.message}`);
}
core.setOutput('message', JSON.stringify(text));
- name: Extracting team name
id: team_name
env:
LABEL_NAME: ${{ github.event.label.name }}
run: |
team_label="${LABEL_NAME//&/$}"
echo "$team_label"
team_name=${team_label#*.Team/}
echo "$team_name"
echo "team_name=${team_name^^}_SLACK_ISSUES_WEBHOOK_URL" >> "$GITHUB_OUTPUT"
- name: Check webhook secret exists
id: check_secret
env:
WEBHOOK_URL: ${{ secrets[steps.team_name.outputs.team_name] }}
run: |
if [ -z "$WEBHOOK_URL" ]; then
echo "Secret ${{ steps.team_name.outputs.team_name }} is not set, skipping Slack notification"
echo "exists=false" >> "$GITHUB_OUTPUT"
else
echo "exists=true" >> "$GITHUB_OUTPUT"
fi
- name: Send the issue ${{ github.event.issue.number }} to Slack
if: steps.check_secret.outputs.exists == 'true'
id: slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets[steps.team_name.outputs.team_name] }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ${{ steps.vars.outputs.message }}
}
}
]
}

View File

@@ -25,7 +25,6 @@ on:
- sqlserver
permissions:
id-token: write
contents: read
checks: write
actions: read
@@ -61,18 +60,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-bigquery-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -121,18 +111,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-clickhouse-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -171,18 +152,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -225,18 +197,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -280,18 +243,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -325,18 +279,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-redshift-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -379,18 +324,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-snowflake-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner
@@ -431,18 +367,9 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlserver-transforms'
granular-rerun: 'false'
test-args: >-
:only-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
role-to-assume: ${{ secrets.CI_TEST_RESULTS_IAM_ROLE }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
- name: Cleanup python-runner
if: always()
uses: ./.github/actions/cleanup-python-runner

View File

@@ -59,7 +59,7 @@ jobs:
run: | # sh
git config user.name github-actions
git config user.email github-actions@github.com
git checkout -b $BRANCH_NAME
git checkout -b "$BRANCH_NAME"
DATE=$(date +'%Y-%m-%d')
@@ -72,7 +72,7 @@ jobs:
git add locales/
git commit -a -m "Update translations $Date" --allow-empty --no-verify
git push -u origin $BRANCH_NAME
git push -u origin "$BRANCH_NAME"
- name: Upload i18n violations report
id: upload-violations
@@ -105,7 +105,7 @@ jobs:
URL=$(gh pr create \
--base master \
--head $BRANCH_NAME \
--head "$BRANCH_NAME" \
--title "Update translations $DATE" \
--body "Synchronize translations with Crowdin" \
--label "no-backport" \
@@ -113,7 +113,7 @@ jobs:
PR_NUMBER=${URL##*/}
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Comment violations summary on PR
if: ${{ steps.create_pr.outputs.pr_number && hashFiles('target/i18n-violations.csv') != '' }}

View File

@@ -78,7 +78,7 @@ jobs:
echo "Skipping build: artifacts exist for this commit hash"
fi
echo "should_build=$SHOULD_BUILD" >> $GITHUB_OUTPUT
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
# if this is on a release branch, we need to check the build requirements
get-build-requirements:
@@ -154,10 +154,22 @@ jobs:
name: metabase-bundle-stats-${{ matrix.edition }}-${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}
path: target/bundle-stats
if-no-files-found: warn
- name: Prepare uberjar artifact
uses: ./.github/actions/prepare-uberjar-artifact
- name: Mark with the commit hash
run: git rev-parse --short HEAD > COMMIT-ID
shell: bash
- name: Calculate SHA256 checksum
run: sha256sum ./target/uberjar/metabase.jar > SHA256.sum
shell: bash
- name: Upload JARs as artifact
uses: actions/upload-artifact@v7
with:
edition: ${{ matrix.edition }}
# 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.
name: metabase-${{ matrix.edition }}-${{ github.event.inputs.commit || github.event.pull_request.head.sha || github.sha }}-uberjar
path: |
./target/uberjar/metabase.jar
./COMMIT-ID
./SHA256.sum
check-jar-health:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}

View File

@@ -61,7 +61,7 @@ jobs:
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=$BRANCH_NAME" >> "$GITHUB_ENV"
echo "Branch name will be: $BRANCH_NAME"
- name: Create Pull Request

View File

@@ -1,20 +0,0 @@
name: Whitespace
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
merge_group:
jobs:
whitespace-linter:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: clojure -T:whitespace-linter lint :whitespace-linter/progress-bar false
name: Run Whitespace Linter

View File

@@ -1,34 +0,0 @@
name: YAML
on:
push:
branches:
- 'master'
- 'release-**'
pull_request:
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
yaml: ${{ steps.changes.outputs.yaml }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
yaml-linter:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
if: needs.files-changed.outputs.yaml == 'true'
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- run: bun run lint-yaml

2
.gitignore vendored
View File

@@ -62,6 +62,8 @@ cypress.env.json
/e2e/coverage-manifest
/e2e/coverage/analysis
/e2e/coverage/spec-file-manifest.json
/e2e/coverage/spec-test-manifest.json
/e2e/coverage/openapi.json
/e2e/support/.nyc_output
/e2e/support/coverage
/e2e/support/e2e

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

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