Compare commits

..

5 Commits

Author SHA1 Message Date
Chris Truter
76e4061dc0 Changelog and atomicity for queries 2024-03-26 12:37:39 +02:00
Chris Truter
23861c5304 Moar docstrings 2024-03-26 12:07:00 +02:00
Chris Truter
388e397633 Combine let blocks 2024-03-26 11:59:54 +02:00
Chris Truter
6a4b9e4959 Simplify 2024-03-26 11:57:52 +02:00
Chris Truter
95ffe8e78e Make CSV replacement atomic 2024-03-26 11:42:27 +02:00
8729 changed files with 423404 additions and 1219651 deletions

View File

@@ -1,5 +1,4 @@
;; -*- comment-column: 100; -*-
{:config-paths ["macros" "src"]
{:config-paths ["macros"]
:linters
{:aliased-namespace-symbol {:level :warning}
:invalid-arity {:skip-args [metabase.lib.util.match/match]} ; TODO (cam): can we fix these?
@@ -15,150 +14,11 @@
:redundant-fn-wrapper {:level :warning}
:refer-all {:level :warning, :exclude [clojure.test]}
:single-key-in {:level :warning}
:unsorted-required-namespaces {:level :warning}
:unused-referred-var {:exclude {compojure.core [GET DELETE POST PUT]}}
:use {:level :warning}
:warn-on-reflection {:level :warning}
:metabase/defsetting-must-specify-export {:level :warning}
:metabase/mbql-query-first-arg {:level :warning}
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
:metabase/require-shape-checker {:level :warning}
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning}
:metabase/validate-logging {:level :warning}
:metabase/validate-deftest
{:level :warning
;; calling these functions will result in an error in tests marked `^:parallel`
;;
;; TODO -- these should all be made thread safe if possible or renamed so they end in `!`. Then we can remove them
;; from this list because by default anything ending in `!` is considered to be thread-unsafe
:parallel/unsafe
#{clojure.core/alter-var-root
clojure.core/with-redefs
clojure.core/with-redefs-fn
metabase.actions.test-util/with-actions
metabase.actions.test-util/with-actions-disabled
metabase.actions.test-util/with-actions-enabled
metabase.actions.test-util/with-actions-test-data
metabase.actions.test-util/with-actions-test-data-and-actions-enabled
metabase.actions.test-util/with-actions-test-data-tables
metabase.analytics.snowplow-test/with-fake-snowplow-collector
metabase.email-test/with-expected-messages
metabase.email-test/with-fake-inbox
metabase.test.data.users/with-group
metabase.test.data.users/with-group-for-user
metabase.test.data/with-empty-h2-app-db
metabase.test.util.log/with-log-level
metabase.test.util.misc/with-single-admin-user
metabase.test.util/with-all-users-permission
metabase.test.util/with-discarded-collections-perms-changes
metabase.test.util/with-env-keys-renamed-by
metabase.test.util/with-locale
metabase.test.util/with-model-cleanup
metabase.test.util/with-non-admin-groups-no-root-collection-for-namespace-perms
metabase.test.util/with-non-admin-groups-no-root-collection-perms
metabase.test.util/with-temp-env-var-value
metabase.test.util/with-temp-vals-in-db
metabase.test.util/with-temporary-raw-setting-values
metabase.test.util/with-temporary-setting-values
metabase.test.util/with-user-in-groups
metabase.test/with-actions
metabase.test/with-actions-disabled
metabase.test/with-actions-enabled
metabase.test/with-actions-test-data
metabase.test/with-actions-test-data-and-actions-enabled
metabase.test/with-actions-test-data-tables
metabase.test/with-all-users-permission
metabase.test/with-discarded-collections-perms-changes
metabase.test/with-empty-h2-app-db
metabase.test/with-env-keys-renamed-by
metabase.test/with-expected-messages
metabase.test/with-fake-inbox
metabase.test/with-group
metabase.test/with-group-for-user
metabase.test/with-locale
metabase.test/with-log-level
metabase.test/with-model-cleanup
metabase.test/with-non-admin-groups-no-root-collection-for-namespace-perms
metabase.test/with-non-admin-groups-no-root-collection-perms
metabase.test/with-single-admin-user
metabase.test/with-temp-env-var-value
metabase.test/with-temp-vals-in-db
metabase.test/with-temporary-raw-setting-values
metabase.test/with-temporary-setting-values
metabase.test/with-user-in-groups}
;; these functions are allowed in `^:parallel` tests even tho they end in `!`
:parallel/safe
#{clojure.core/assoc!
clojure.core/compare-and-set!
clojure.core/conj!
clojure.core/disj!
clojure.core/dissoc!
clojure.core/persistent!
clojure.core/pop!
clojure.core/reset!
clojure.core/reset-vals!
clojure.core/run!
clojure.core/swap!
clojure.core/swap-vals!
clojure.core/volatile!
clojure.core/vreset!
clojure.core/vswap!
clojure.core.async/<!
clojure.core.async/<!!
clojure.core.async/>!
clojure.core.async/>!!
clojure.core.async/alt!
clojure.core.async/alt!!
clojure.core.async/alts!
clojure.core.async/alts!!
clojure.core.async/close!
clojure.core.async/ioc-alts!
clojure.core.async/offer!
clojure.core.async/onto-chan!
clojure.core.async/onto-chan!!
clojure.core.async/poll!
clojure.core.async/put!
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
metabase.channel.core/send!
metabase.driver.sql-jdbc.execute/execute-prepared-statement!
metabase.pulse/send-pulse!
metabase.query-processor.store/store-database!
next.jdbc/execute!}}
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests
{:level :warning
:max-length 180}
;; this is not enabled here, it is enabled below only for specific namespaces.
:metabase/disallow-hardcoded-driver-names-in-tests
;; don't include `:h2` since that will probably lead to a mountain of tests where we are specifically testing
;; middleware or compilation behavior with just H2 since it's the default.
{:drivers #{ ;; core drivers
#_:h2
:postgres
:mysql
;; module drivers
:athena
:bigquery-cloud-sdk
:databricks
:druid
:druid-jdbc
:googleanalytics
:mongo
:oracle
:presto-jdbc
:redshift
:snowflake
:sparksql
:sqlite
:sqlserver
:vertica}}
:metabase/defsetting-must-specify-export {:level :warning}
;;
;; disabled linters
@@ -174,7 +34,6 @@
;; :docstring-leading-trailing-whitespace {:level :warning}
;; :docstring-no-summary {:level :warning}
;; :shadowed-var {:level :warning}
;; :def-fn {:level :warning}
:unresolved-symbol
{:exclude
@@ -183,7 +42,7 @@
(clojure.core.logic/fresh)
(clojure.core.logic/matcha)
(clojure.core.logic/run)
(clojure.test/is [query= =? malli= qv=])
(clojure.test/is [query= =? malli=])
(clojure.tools.macro/macrolet)
instaparse.core/transform
(metabase.async.streaming-response/streaming-response)
@@ -237,7 +96,6 @@
malli.core/explainer {:message "Use metabase.util.malli.registry/explainer instead of malli.core/explainer"}
me.flowthing.pp/pprint {:message "Use metabase.util.log instead of me.flowthing.pp/pprint"}
medley.core/random-uuid {:message "Use clojure.core/random-uuid instead of medley.core/random-uuid"}
metabase.driver/database-supports? {:message "Use metabase.driver.util/supports? instead of metabase.driver/database-supports?"}
metabase.lib.equality/find-column-indexes-for-refs {:message "Use lib.equality/closest-matches-in-metadata or lib.equality/find-closest-matches-for-refs instead of lib.equality/find-column-indexes-for-refs"}
metabase.test/with-temp* {:message "Use mt/with-temp instead of mt/with-temp*"}
toucan2.tools.with-temp {:message "Use mt/with-temp instead of t2.with-temp/with-temp"}}
@@ -245,7 +103,6 @@
:discouraged-namespace
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
clojure.tools.logging {:message "Use metabase.util.log instead of clojure.tools.logging directly"}
grouper.core {:message "Use metabase.util.grouper instead of grouper.core"}
honeysql.core {:message "Use Honey SQL 2. Honey SQL 1 is removed as a dependency in Metabase 49+."}
honeysql.format {:message "Use Honey SQL 2. Honey SQL 1 is removed as a dependency in Metabase 49+."}
honeysql.helpers {:message "Use Honey SQL 2. Honey SQL 1 is removed as a dependency in Metabase 49+."}
@@ -257,207 +114,7 @@
{:exclude
[colorize.core]}
;; A "module" is any `metabase.x` namespace in `src`.
:metabase/ns-module-checker
{:level :warning
;; Map of module name => the "core" external public-facing API namespace(s). You have three options here:
;;
;; 1. Special sentinel value `:any` means means this module does not (yet) have external public-facing API
;; namespace(s). This is mostly a temporary placeholder until we go in and create module namespaces, which means
;; you should go create one.
;;
;; 2. A set of namespace symbols. All namespaces in other modules will only be allowed to use namespaces from
;; this set. Ideally this set should only have one namespace, but restricting it to a set of several is still
;; better than `:any`.
;;
;; 3. `nil` or not listed here -- we default to assuming there is one API namespace that matches the name of the
;; module itself, e.g. the module namespace for `metabase.setup` would be `metabase.setup`.
;;
;; `nil` or an empty set Otherwise this should be a set of namespace symbols.
;;
;; PRO TIP: Check out the `dev.deps-graph` namespace for helpful tools to see where a module is used externally.
:api-namespaces
{metabase.actions #{metabase.actions.core}
metabase.analytics #{metabase.analytics.prometheus
metabase.analytics.snowplow
metabase.analytics.stats
metabase.analytics.sdk} ; TODO -- consolidate these into a real API namespace.
metabase.analyze #{metabase.analyze}
metabase.api #{metabase.api.common
metabase.api.dataset
metabase.api.permission-graph
metabase.api.routes} ; TODO -- consolidate these into a real API namespace. I think the `*current-user*` type stuff might need to be moved into a separate module.
metabase.async #{metabase.async.streaming-response
metabase.async.streaming-response.thread-pool
metabase.async.util} ; TODO -- consolidate these into a real API namespace.
metabase.audit #{metabase.audit}
metabase.bootstrap #{metabase.bootstrap}
metabase.cmd #{} ; there are no namespaces here since you shouldn't be using this in any other module.
metabase.channel #{metabase.channel.core}
metabase.config #{metabase.config}
metabase.core #{metabase.core.initialization-status} ; TODO -- only namespace used outside of EE, this probably belongs in `metabase.server` anyway since that's the only place it's used.
metabase.db #{metabase.db
metabase.db.metadata-queries ; TODO this should probably be separate from metabase.db
metabase.db.query ; TODO this is mostly util stuff like `metabase.db.query/query` that we don't even need anymore.
metabase.db.setup} ; TODO these are only calling `metabase.db.setup/setup-db!` and there's a slightly different version in `metabase.db`
metabase.driver :any ; TODO -- 19 namespaces!!!! CRY
metabase.email #{metabase.email
metabase.email.messages} ; TODO -- consolidate these into a real API namespace.
metabase.embed #{metabase.embed.settings}
metabase.events #{metabase.events}
metabase.formatter #{metabase.formatter}
metabase.integrations #{metabase.integrations.common
metabase.integrations.google
metabase.integrations.ldap
metabase.integrations.slack} ; TODO -- consolidate these into a real API namespace.
metabase.legacy-mbql #{metabase.legacy-mbql.normalize
metabase.legacy-mbql.predicates
metabase.legacy-mbql.schema
metabase.legacy-mbql.schema.helpers
metabase.legacy-mbql.util} ; TODO -- consolidate these into a real API namespace.
metabase.lib :any ; TODO -- :cry: 34 externally referenced namespaces, but maybe half of them are schema namespaces which technically don't need to be required.
metabase.logger #{metabase.logger}
metabase.metabot #{metabase.metabot}
metabase.models :any ; TODO -- scream, 62 namespaces used elsewhere, but to be fair a lot of these don't *need* to be required.
metabase.moderation #{metabase.moderation}
metabase.permissions #{metabase.permissions.util} ; TODO -- this is currently the only namespace in this module. Give it a real API namespace?
metabase.plugins #{metabase.plugins
metabase.plugins.classloader} ; TODO -- not 100% sure the classloader belongs here
metabase.public-settings #{metabase.public-settings
metabase.public-settings.premium-features}
metabase.pulse #{metabase.pulse
metabase.pulse.markdown
metabase.pulse.parameters
metabase.pulse.preview
metabase.pulse.render
metabase.pulse.render.image-bundle
metabase.pulse.render.js-svg
metabase.pulse.render.style} ; TODO -- consolidate these into a real API namespace.
metabase.query-processor :any ; TODO omg scream, 29 namespaces used outside of the module. WHAT THE HECC
metabase.query-analysis #{metabase.query-analysis
metabase.query-analysis.failure-map}
metabase.related #{metabase.related}
metabase.sample-data #{metabase.sample-data}
metabase.search #{metabase.search}
metabase.server #{metabase.server
metabase.server.handler
metabase.server.middleware.auth
metabase.server.middleware.exceptions
metabase.server.middleware.json
metabase.server.middleware.misc
metabase.server.middleware.offset-paging
metabase.server.middleware.session
metabase.server.protocols
metabase.server.request.util} ; TODO -- consolidate these into a real API namespace.
metabase.setup #{metabase.setup}
metabase.shared #{metabase.shared.dashboards.constants
metabase.shared.formatting.constants
metabase.shared.formatting.date
metabase.shared.formatting.numbers
metabase.shared.models.visualization-settings
metabase.shared.parameters.parameters
metabase.shared.util.currency
metabase.shared.util.i18n
metabase.shared.util.internal.time-common
metabase.shared.util.namespaces
metabase.shared.util.time} ; TODO -- consolidate these into a real API namespace.
metabase.sync #{metabase.sync
metabase.sync.analyze
metabase.sync.concurrent
metabase.sync.field-values
metabase.sync.schedules
metabase.sync.sync-metadata
metabase.sync.sync-metadata.fields
metabase.sync.sync-metadata.tables
metabase.sync.util} ; TODO -- consolidate these into a real API namespace.
metabase.task #{metabase.task
metabase.task.index-values
metabase.task.persist-refresh
;; We need to expose this to the metabase.search module.
;; Ideally, it would just live inside that module.
metabase.task.search-index} ; TODO -- consolidate these into a real API namespace.
metabase.troubleshooting #{metabase.troubleshooting}
metabase.types #{metabase.types}
metabase.upload #{metabase.upload}
metabase.util :any ; I think util being `:any` is actually something I am ok with. But this has 32 namespaces.
metabase.xrays #{metabase.xrays}}
;; Map of module => other modules you're allowed to use there. You have two options here:
;;
;; 1. `:any` means namespaces in this module are allowed to use any other module -- allowed modules are not
;; enforced for this module. Module API namespaces for modules that have them defined are still enforced. For
;; ones that are `nil`, please go in and add a list of allowed modules. `:any` is mostly meant a temporary
;; placeholder until we can fill these all out, so feel free to fix these.
;;
;; 2. A set of module symbols. This is the list of modules that are allowed to be referenced. An empty set means no
;; other modules are allowed to be referenced; this is the default for any modules that aren't listed here.
:allowed-modules
{metabase.actions :any
metabase.analytics :any
metabase.analyze :any
metabase.api :any
metabase.async :any
metabase.audit :any
metabase.auth-provider #{metabase.util}
metabase.bootstrap #{}
metabase.cmd :any
metabase.compatibility :any
metabase.channel :any
metabase.config #{metabase.plugins}
metabase.core :any
metabase.db :any
metabase.driver :any
metabase.email :any
metabase.embed :any
metabase.events :any
metabase.formatter :any
metabase.integrations :any
metabase.legacy-mbql :any
metabase.lib :any
metabase.logger #{metabase.config
metabase.plugins}
metabase.metabot :any
metabase.models :any
metabase.moderation :any
metabase.permissions :any
metabase.plugins :any
metabase.public-settings :any
metabase.pulse :any
metabase.query-analysis #{metabase.config
metabase.driver
metabase.legacy-mbql
metabase.lib
metabase.models
metabase.public-settings
metabase.query-processor
metabase.util}
metabase.query-processor :any
metabase.related :any
metabase.sample-data :any
metabase.search :any
metabase.server :any
metabase.setup :any
metabase.shared :any
metabase.sync :any
metabase.task :any
metabase.troubleshooting :any
metabase.types #{metabase.util}
metabase.upload :any
metabase.util :any
metabase.xrays :any}
;; namespaces matching these patterns (with `re-find`) are excluded from the module linter. Since regex literals
;; aren't allowed in EDN just used the `(str <regex>)` version i.e. two slashes instead of one.
;;
;; this is mostly intended for excluding test namespaces or those rare 'glue' namespaces that glue multiple modules
;; together, e.g. `metabase.lib.metadata.jvm`.
:ignored-namespace-patterns
#{"-test$" ; anything ending in `-test`
"test[-.]util" ; anything that contains `test.util` or `test-util`
"test\\.impl" ; anything that contains `test.impl`
"^metabase\\.test" ; anything starting with `metabase.test`
"^metabase\\.http-client$"}} ; `metabase.http-client` which is a test-only namespace despite its name.
:unsorted-required-namespaces {:level :warning}
:consistent-alias
{:aliases
@@ -487,8 +144,12 @@
hiccup.util hiccup.util
honey.sql sql
honey.sql.helpers sql.helpers
java-time.api t
macaw.core macaw
honeysql.core hsql
honeysql.format hformat
honeysql.helpers hh
honeysql.types htypes
java-time t
macaw.core mac
malli.core mc
malli.error me
malli.experimental mx
@@ -500,17 +161,7 @@
metabase-enterprise.audit-app.pages.common common
metabase-enterprise.sandbox.api.table table
metabase-enterprise.test met
metabase.analytics.sdk sdk
metabase.analytics.stats stats
metabase.analyze.query-results qr
metabase.analyze.classifiers.category classifiers.category
metabase.analyze.classifiers.core classifiers
metabase.analyze.classifiers.name classifiers.name
metabase.analyze.classifiers.no-preview-display classifiers.no-preview-display
metabase.analyze.classifiers.text-fingerprint classifiers.text-fingerprint
metabase.analyze.fingerprint.fingerprinters fingerprinters
metabase.analyze.fingerprint.insights insights
metabase.analyze.fingerprint.schema fingerprint.schema
metabase.api.activity api.activity
metabase.api.alert api.alert
metabase.api.automagic-dashboards api.magic
@@ -552,10 +203,13 @@
metabase.api.tiles api.tiles
metabase.api.timeline api.timeline
metabase.api.timeline-event api.timeline-event
metabase.api.transform api.transform
metabase.api.user api.user
metabase.api.util api.util
metabase.async.streaming-response.thread-pool thread-pool
metabase.channel.core channel
metabase.automagic-dashboards.core magic
metabase.automagic-dashboards.dashboard-templates dashboard-templates
metabase.automagic-dashboards.populate populate
metabase.cmd.copy.h2 copy.h2
metabase.config config
metabase.config.file config.file
@@ -565,6 +219,7 @@
metabase.db.metadata-queries metadata-queries
metabase.db.spec mdb.spec
metabase.db.query mdb.query
metabase.domain-entities.specs de.specs
metabase.driver.common.parameters params
metabase.driver.common.parameters.dates params.dates
metabase.driver.common.parameters.operators params.ops
@@ -575,6 +230,10 @@
metabase.driver.druid.js druid.js
metabase.driver.druid.query-processor druid.qp
metabase.driver.druid.sync druid.sync
metabase.driver.googleanalytics.client ga.client
metabase.driver.googleanalytics.execute ga.execute
metabase.driver.googleanalytics.metadata ga.metadata
metabase.driver.googleanalytics.query-processor ga.qp
metabase.driver.impl driver.impl
metabase.driver.mongo.execute mongo.execute
metabase.driver.mongo.parameters mongo.params
@@ -603,14 +262,12 @@
metabase.legacy-mbql.schema mbql.s
metabase.legacy-mbql.util mbql.u
metabase.lib.util lib.util
metabase.lib.util.match lib.util.match
metabase.models.activity activity
metabase.models.application-permissions-revision a-perm-revision
metabase.models.bookmark bookmark
metabase.models.collection collection
metabase.models.collection.graph graph
metabase.models.collection-permission-graph-revision c-perm-revision
metabase.models.channel models.channel
metabase.models.dashboard-card dashboard-card
metabase.models.database database
metabase.models.dependency dependency
@@ -631,8 +288,7 @@
metabase.models.setting.cache setting.cache
metabase.models.timeline timeline
metabase.models.timeline-event timeline-event
metabase.analysis.native-query-analyzer nqa
metabase.analysis.native-query-analyzer.parameter-substitution nqa.sub
metabase.native-query-analyzer query-analyzer
metabase.plugins.initialize plugins.init
metabase.public-settings public-settings
metabase.public-settings.premium-features premium-features
@@ -686,7 +342,14 @@
metabase.server.protocols server.protocols
metabase.shared.util shared.u
metabase.shared.util.currency currency
metabase.sync.analyze.fingerprint sync.fingerprint
metabase.sync.analyze.classifiers.category classifiers.category
metabase.sync.analyze.classifiers.name classifiers.name
metabase.sync.analyze.classifiers.no-preview-display classifiers.no-preview-display
metabase.sync.analyze.classifiers.text-fingerprint classifiers.text-fingerprint
metabase.sync.analyze.fingerprint fingerprint
metabase.sync.analyze.fingerprint.fingerprinters fingerprinters
metabase.sync.analyze.fingerprint.insights insights
metabase.sync.analyze.query-results qr
metabase.sync.field-values field-values
metabase.sync.interface i
metabase.sync.schedules sync.schedules
@@ -706,6 +369,9 @@
metabase.test.util.js test.js
metabase.test.util.timezone test.tz
metabase.timeseries-query-processor-test.util tqpt
metabase.transforms.core tf
metabase.transforms.materialize tf.materialize
metabase.transforms.specs tf.specs
metabase.util.cron u.cron
metabase.util.date-2 u.date
metabase.util.date-2.common u.date.common
@@ -714,7 +380,6 @@
metabase.util.encryption encryption
metabase.util.encryption-test encryption-test
metabase.util.files u.files
metabase.util.grouper grouper
metabase.util.honey-sql-2 h2x
metabase.util.i18n i18n
metabase.util.i18n.impl i18n.impl
@@ -727,7 +392,6 @@
metabase.util.schema su
metabase.util.ui-logic ui-logic
metabase.util.urls urls
metabase.xrays xrays
metabuild-common.core u
metabuild-common.output out
metabuild-common.shell shell
@@ -738,118 +402,114 @@
ring.adapter.jetty9.servlet servlet
saml20-clj.core saml
toucan.db db
toucan.models models}}}
toucan.models models}}
:metabase/validate-deftest {:level :warning}
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests {:level :warning}
:metabase/warn-about-schema= {:level :warning}}
:lint-as
{cljs.cache/defcache clojure.core/deftype
clojure.core.cache/defcache clojure.core/deftype
clojure.core.logic/defne clj-kondo.lint-as/def-catch-all
clojure.test.check.clojure-test/defspec clojure.test/deftest
clojurewerkz.quartzite.jobs/defjob clojure.core/defn
metabase-enterprise.serialization.test-util/with-dbs clojure.core/fn
metabase-enterprise.serialization.test-util/with-random-dump-dir clojure.core/let
metabase.actions.test-util/with-actions clojure.core/let
metabase.api.common/defroutes clojure.core/def
metabase.api.common/let-404 clojure.core/let
metabase.api.search-test/do-test-users clojure.core/let
metabase.async.api-response-test/with-response clojure.core/let
metabase.dashboard-subscription-test/with-dashboard-sub-for-card clojure.core/let
metabase.channel.http-test/with-captured-http-requests clojure.core/fn
metabase.channel.http-test/with-server clojure.core/let
metabase.db.custom-migrations/define-migration clj-kondo.lint-as/def-catch-all
metabase.db.custom-migrations/define-reversible-migration clj-kondo.lint-as/def-catch-all
metabase.db.data-migrations/defmigration clojure.core/def
metabase.db.liquibase/with-liquibase clojure.core/let
metabase.db.schema-migrations-test.impl/with-temp-empty-app-db clojure.core/let
metabase.driver.mongo.connection/with-mongo-client clojure.core/let
metabase.driver.mongo.connection/with-mongo-database clojure.core/let
metabase.driver.mongo.query-processor/mongo-let clojure.core/let
metabase.driver.sql-jdbc.actions/with-jdbc-transaction clojure.core/let
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection clojure.core/let
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info clojure.core/fn
metabase.integrations.ldap/with-ldap-connection clojure.core/fn
metabase.models.collection-test/with-collection-in-location clojure.core/let
metabase.models.json-migration/def-json-migration clj-kondo.lint-as/def-catch-all
metabase.models.setting.multi-setting/define-multi-setting clojure.core/def
metabase.models.setting/defsetting clj-kondo.lint-as/def-catch-all
metabase.public-settings.premium-features/defenterprise-schema clj-kondo.lint-as/def-catch-all
metabase.public-settings.premium-features/define-premium-feature clojure.core/def
metabase.pulse-test/with-pulse-for-card clojure.core/let
metabase.query-processor-test.pipeline-queries-test/pmbql-query clojure.core/->
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query clojure.core/->
metabase.query-processor.error-type/deferror clojure.core/def
metabase.query-processor.middleware.cache.impl/with-reducible-deserialized-results clojure.core/let
metabase.query-processor.middleware.process-userland-query-test/with-query-execution! clojure.core/let
metabase.query-processor.setup/with-qp-setup clojure.core/let
metabase.shared.util.namespaces/import-fns potemkin/import-vars
metabase.sync.util/sum-for clojure.core/for
metabase.sync.util/with-emoji-progress-bar clojure.core/let
metabase.test.data.interface/defdataset clojure.core/def
metabase.test.data.interface/defdataset-edn clojure.core/def
metabase.test/defdataset clojure.core/def
metabase.test/let-url clojure.core/let
metabase.test/with-actions clojure.core/let
metabase.test/with-grouper-batches! clojure.core/fn
metabase.test/with-open-channels clojure.core/let
metabase.test/with-single-admin-user clojure.core/fn
metabase.test/with-temp-dir clojure.core/let
metabase.test/with-temp-empty-app-db clojure.core/let
metabase.test/with-temp-file clojure.core/let
metabase.test/with-user-in-groups clojure.core/let
metabase.upload-test/with-upload-table! clojure.core/let
metabase.util.files/with-open-path-to-resource clojure.core/let
metabase.util.malli.defn/defn schema.core/defn
metabase.util.malli.defn/defn- schema.core/defn
metabase.util.malli.fn/fn schema.core/fn
metabase.util.malli/defmethod schema.core/defmethod
metabase.util.malli/defn schema.core/defn
metabase.util.malli/defn- schema.core/defn
metabase.util.malli/fn schema.core/fn
metabase.util.ssh/with-ssh-tunnel clojure.core/let
metabase.xrays.domain-entities.malli/defn schema.core/defn
monger.operators/defoperator clojure.core/def
potemkin.types/defprotocol+ clojure.core/defprotocol
potemkin.types/defrecord+ clojure.core/defrecord
potemkin.types/deftype+ clojure.core/deftype
potemkin/defprotocol+ clojure.core/defprotocol
potemkin/defrecord+ clojure.core/defrecord
potemkin/deftype+ clojure.core/deftype
toucan.db/with-call-counting clojure.core/fn
toucan2.core/with-call-count clojure.core/fn}
{clojure.core.logic/defne clj-kondo.lint-as/def-catch-all
clojure.test.check.clojure-test/defspec clojure.test/deftest
clojurewerkz.quartzite.jobs/defjob clojure.core/defn
honeysql.helpers/defhelper clj-kondo.lint-as/def-catch-all
honeysql.util/defalias clojure.core/def
metabase-enterprise.serialization.test-util/with-random-dump-dir clojure.core/let
metabase-enterprise.serialization.test-util/with-dbs clojure.core/fn
metabase.actions.test-util/with-actions clojure.core/let
metabase.api.common/let-404 clojure.core/let
metabase.api.common/defroutes clojure.core/def
metabase.api.search-test/do-test-users clojure.core/let
metabase.async.api-response-test/with-response clojure.core/let
metabase.dashboard-subscription-test/with-dashboard-sub-for-card clojure.core/let
metabase.db.custom-migrations/define-migration clj-kondo.lint-as/def-catch-all
metabase.db.custom-migrations/define-reversible-migration clj-kondo.lint-as/def-catch-all
metabase.db.data-migrations/defmigration clojure.core/def
metabase.db.liquibase/with-liquibase clojure.core/let
metabase.db.schema-migrations-test.impl/with-temp-empty-app-db clojure.core/let
metabase.domain-entities.malli/defn schema.core/defn
metabase.driver.mongo.query-processor/mongo-let clojure.core/let
metabase.driver.mongo.connection/with-mongo-client clojure.core/let
metabase.driver.mongo.connection/with-mongo-database clojure.core/let
metabase.driver.sql-jdbc.actions/with-jdbc-transaction clojure.core/let
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection clojure.core/let
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info clojure.core/fn
metabase.integrations.ldap/with-ldap-connection clojure.core/fn
metabase.models.collection-test/with-collection-in-location clojure.core/let
metabase.models.json-migration/def-json-migration clj-kondo.lint-as/def-catch-all
metabase.models.setting.multi-setting/define-multi-setting clojure.core/def
metabase.models.setting/defsetting clj-kondo.lint-as/def-catch-all
metabase.public-settings.premium-features/defenterprise-schema clj-kondo.lint-as/def-catch-all
metabase.public-settings.premium-features/define-premium-feature clojure.core/def
metabase.pulse-test/with-pulse-for-card clojure.core/let
metabase.query-processor.error-type/deferror clojure.core/def
metabase.query-processor.setup/with-qp-setup clojure.core/let
metabase.query-processor.middleware.cache.impl/with-reducible-deserialized-results clojure.core/let
metabase.query-processor.middleware.process-userland-query-test/with-query-execution clojure.core/let
metabase.query-processor-test.pipeline-queries-test/pmbql-query clojure.core/->
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query clojure.core/->
metabase.shared.util.namespaces/import-fns potemkin/import-vars
metabase.sync.util/sum-for clojure.core/for
metabase.sync.util/with-emoji-progress-bar clojure.core/let
metabase.test.data.interface/defdataset clojure.core/def
metabase.test.data.interface/defdataset-edn clojure.core/def
metabase.test/defdataset clojure.core/def
metabase.test/let-url clojure.core/let
metabase.test/with-actions clojure.core/let
metabase.test/with-open-channels clojure.core/let
metabase.test/with-single-admin-user clojure.core/fn
metabase.test/with-temp-dir clojure.core/let
metabase.test/with-temp-empty-app-db clojure.core/let
metabase.test/with-temp-file clojure.core/let
metabase.test/with-user-in-groups clojure.core/let
metabase.upload-test/with-upload-table! clojure.core/let
metabase.util.files/with-open-path-to-resource clojure.core/let
metabase.util.malli.defn/defn schema.core/defn
metabase.util.malli.fn/fn schema.core/fn
metabase.util.malli/defmethod schema.core/defmethod
metabase.util.malli/defn schema.core/defn
metabase.util.malli/fn schema.core/fn
metabase.util.ssh/with-ssh-tunnel clojure.core/let
monger.operators/defoperator clojure.core/def
potemkin.types/defprotocol+ clojure.core/defprotocol
potemkin.types/defrecord+ clojure.core/defrecord
potemkin.types/deftype+ clojure.core/deftype
potemkin/defprotocol+ clojure.core/defprotocol
potemkin/defrecord+ clojure.core/defrecord
potemkin/deftype+ clojure.core/deftype
toucan.db/with-call-counting clojure.core/fn
toucan2.core/with-call-count clojure.core/fn}
:hooks
{:analyze-call
{cljs.test/deftest hooks.clojure.test/deftest
cljs.test/is hooks.clojure.test/is
cljs.test/use-fixtures hooks.clojure.test/use-fixtures
clojure.core/ns hooks.clojure.core/lint-ns
clojure.test/deftest hooks.clojure.test/deftest
clojure.test/is hooks.clojure.test/is
clojure.test/use-fixtures hooks.clojure.test/use-fixtures
metabase-enterprise.cache.config-test/with-temp-persist-models hooks.common/with-seven-bindings
metabase-enterprise.advanced-permissions.models.permissions.application-permissions-test/with-new-group-and-current-graph hooks.common/with-two-top-level-bindings
metabase-enterprise.audit-app.pages-test/with-temp-objects hooks.common/with-one-binding
metabase-enterprise.cache.config-test/with-temp-persist-models hooks.common/with-seven-bindings
metabase-enterprise.serialization.test-util/with-temp-dpc hooks.toucan2.tools.with-temp/with-temp
metabase.analytics.prometheus-test/with-prometheus-system! hooks.common/with-two-bindings
metabase.api.alert-test/with-alert-in-collection! hooks.common/with-four-bindings
metabase.api.automagic-dashboards-test/with-indexed-model! hooks.metabase.api.automagic-dashboards-test/with-indexed-model!
metabase.api.card-test/with-card-param-values-fixtures hooks.common/let-one-with-optional-value
metabase.api.card-test/with-persistence-setup! hooks.common/with-one-top-level-binding
metabase.analytics.prometheus-test/with-prometheus-system hooks.common/with-two-bindings
metabase.api.automagic-dashboards-test/with-indexed-model hooks.metabase.api.automagic-dashboards-test/with-indexed-model
metabase.api.alert-test/with-alert-in-collection hooks.common/with-four-bindings
metabase.api.card-test/with-persistence-setup hooks.common/with-one-top-level-binding
metabase.api.card-test/with-temp-native-card! hooks.common/with-two-bindings
metabase.api.card-test/with-temp-native-card-with-params! hooks.common/with-two-bindings
metabase.api.collection-test/with-french-user-and-personal-collection! hooks.common/with-two-top-level-bindings
metabase.api.common/defendpoint hooks.metabase.api.common/defendpoint
metabase.api.common/defendpoint-async hooks.metabase.api.common/defendpoint
metabase.api.common/defendpoint-async-schema hooks.metabase.api.common/defendpoint
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
metabase.api.collection-test/with-french-user-and-personal-collection hooks.common/with-two-top-level-bindings
metabase.api.dashboard-test/with-chain-filter-fixtures hooks.common/let-one-with-optional-value
metabase.api.dashboard-test/with-simple-dashboard-with-tabs hooks.common/with-one-binding
metabase.api.card-test/with-card-param-values-fixtures hooks.common/let-one-with-optional-value
metabase.api.embed-test/do-response-formats hooks.common/with-two-bindings
metabase.api.embed-test/with-chain-filter-fixtures! hooks.common/let-one-with-optional-value
metabase.api.embed-test/with-chain-filter-fixtures hooks.common/let-one-with-optional-value
metabase.api.embed-test/with-temp-card hooks.common/let-one-with-optional-value
metabase.api.embed-test/with-temp-dashcard hooks.common/let-one-with-optional-value
metabase.api.persist-test/with-setup! hooks.common/with-one-top-level-binding
metabase.api.public-test/with-required-param-card! hooks.common/with-one-binding
metabase.api.persist-test/with-setup hooks.common/with-one-top-level-binding
metabase.api.public-test/with-required-param-card hooks.common/with-one-binding
metabase.api.public-test/with-temp-public-card hooks.common/let-one-with-optional-value
metabase.api.public-test/with-temp-public-dashboard hooks.common/let-one-with-optional-value
metabase.api.public-test/with-temp-public-dashboard-and-card hooks.common/with-three-bindings
@@ -857,34 +517,32 @@
metabase.api.search-test/with-search-items-in-root-collection hooks.common/do*
metabase.api.user-test/with-temp-user-email hooks.common/with-one-binding
metabase.async.streaming-response-test/with-start-execution-chan hooks.common/with-one-binding
metabase.dashboard-subscription-test/with-link-card-fixture-for-dashboard hooks.common/let-second
metabase.db.schema-migrations-test.impl/test-migrations hooks.metabase.db.schema-migrations-test.impl/test-migrations
metabase.dashboard-subscription-test/with-link-card-fixture-for-dashboard hooks.common/let-second
metabase.driver.bigquery-cloud-sdk-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
metabase.driver/register! hooks.metabase.driver/register!
metabase.legacy-mbql.schema.macros/defclause hooks.metabase.legacy-mbql.schemas.macros/defclause
metabase.lib.schema.mbql-clause/define-catn-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
metabase.lib.schema.mbql-clause/define-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
metabase.lib.schema.mbql-clause/define-catn-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
metabase.lib.schema.mbql-clause/define-tuple-mbql-clause hooks.metabase.lib.schema.mbql-clause/define-mbql-clause
metabase.lib.test-util.macros/$ids hooks.metabase.test.data/$ids
metabase.lib.test-util.macros/mbql-query hooks.metabase.test.data/mbql-query
metabase.lib.test-util.macros/with-testing-against-standard-queries hooks.metabase.lib.test-util.macros/with-testing-against-standard-queries
metabase.models.collection-test/with-collection-hierarchy! hooks.common/let-one-with-optional-value
metabase.models.collection-test/with-collection-hierarchy hooks.common/let-one-with-optional-value
metabase.models.collection-test/with-personal-and-impersonal-collections hooks.common/with-two-bindings
metabase.models.dashboard-test/with-dash-in-collection hooks.common/with-three-bindings
metabase.models.dashboard-tab-test/with-dashtab-in-personal-collection hooks.common/with-one-top-level-binding
metabase.models.dashboard-test/with-dash-in-collection! hooks.common/with-three-bindings
metabase.models.interface/define-batched-hydration-method hooks.metabase.models.interface/define-hydration-method
metabase.models.interface/define-simple-hydration-method hooks.metabase.models.interface/define-hydration-method
metabase.models.pulse-test/with-dashboard-subscription-in-collection! hooks.common/with-four-bindings
metabase.models.pulse-test/with-pulse-in-collection! hooks.common/with-four-bindings
metabase.models.interface/define-batched-hydration-method hooks.metabase.models.interface/define-hydration-method
metabase.models.pulse-test/with-dashboard-subscription-in-collection hooks.common/with-four-bindings
metabase.models.pulse-test/with-pulse-in-collection hooks.common/with-four-bindings
metabase.models.setting.multi-setting/define-multi-setting hooks.metabase.models.setting/define-multi-setting
metabase.models.setting/defsetting hooks.metabase.models.setting/defsetting
metabase.public-settings.premium-features/defenterprise hooks.metabase.public-settings.premium-features/defenterprise
metabase.pulse.test-util/checkins-query-card hooks.metabase.test.data/$ids
metabase.query-processor-test.expressions-test/calculate-bird-scarcity hooks.metabase.query-processor-test.expressions-test/calculate-bird-scarcity
metabase.query-processor-test.filter-test/count-with-filter-clause hooks.metabase.test.data/$ids
metabase.query-processor.middleware.cache-test/with-mock-cache! hooks.common/with-two-bindings
metabase.query-processor.middleware.cache-test/with-mock-cache hooks.common/with-two-bindings
metabase.sample-data-test/with-temp-sample-database-db hooks.common/with-one-binding
metabase.stale-test/with-stale-items hooks.toucan2.tools.with-temp/with-temp
metabase.test.data.users/with-group hooks.common/let-one-with-optional-value
metabase.test.data/$ids hooks.metabase.test.data/$ids
metabase.test.data/dataset hooks.metabase.test.data/dataset
@@ -892,6 +550,7 @@
metabase.test.data/run-mbql-query hooks.metabase.test.data/mbql-query
metabase.test.util.async/with-open-channels hooks.common/let-with-optional-value-for-last-binding
metabase.test.util.log/with-log-level hooks.common/with-ignored-first-arg
metabase.test.util.log/with-log-messages-for-level hooks.common/with-ignored-first-arg
metabase.test.util/discard-setting-changes hooks.common/with-ignored-first-arg
metabase.test.util/with-column-remappings hooks.common/with-ignored-first-arg
metabase.test.util/with-non-admin-groups-no-root-collection-perms hooks.common/do*
@@ -902,70 +561,54 @@
metabase.test/discard-setting-changes hooks.common/with-ignored-first-arg
metabase.test/mbql-query hooks.metabase.test.data/mbql-query
metabase.test/query hooks.metabase.test.data/mbql-query
metabase.test/run-mbql-query hooks.metabase.test.data/mbql-query
metabase.test/test-drivers hooks.common/do*
metabase.test/run-mbql-query hooks.metabase.test.data/mbql-query
metabase.test/with-column-remappings hooks.common/with-ignored-first-arg
metabase.test/with-group hooks.common/let-one-with-optional-value
metabase.test/with-log-level hooks.common/with-ignored-first-arg
metabase.test/with-log-messages-for-level hooks.metabase.util.log.capture/with-log-messages-for-level
metabase.test/with-log-messages-for-level hooks.common/with-ignored-first-arg
metabase.test/with-non-admin-groups-no-root-collection-perms hooks.common/do*
metabase.test/with-temp hooks.toucan2.tools.with-temp/with-temp
metabase.test/with-temp-file hooks.metabase.test.util/with-temp-file
metabase.test/with-temporary-setting-values hooks.metabase.test.util/with-temporary-setting-values
metabase.util.log.capture/with-log-messages-for-level hooks.metabase.util.log.capture/with-log-messages-for-level
metabase.util.log/debug hooks.metabase.util.log/info
metabase.util.log/debugf hooks.metabase.util.log/infof
metabase.util.log/error hooks.metabase.util.log/info
metabase.util.log/errorf hooks.metabase.util.log/infof
metabase.util.log/fatal hooks.metabase.util.log/info
metabase.util.log/fatalf hooks.metabase.util.log/infof
metabase.util.log/info hooks.metabase.util.log/info
metabase.util.log/infof hooks.metabase.util.log/infof
metabase.util.log/trace hooks.metabase.util.log/info
metabase.util.log/tracef hooks.metabase.util.log/infof
metabase.util.log/warn hooks.metabase.util.log/info
metabase.util.log/warnf hooks.metabase.util.log/infof
metabase.util.malli.registry/def hooks.metabase.util.malli.registry/def
metabase.util/format-color hooks.metabase.util/format-color}
metabase.util.malli.registry/def hooks.metabase.util.malli.registry/def}
:macroexpand
{clojurewerkz.quartzite.jobs/build macros.quartz/build-job
clojurewerkz.quartzite.schedule.cron/schedule macros.quartz/schedule
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.serialization.test-util/with-world macros.metabase-enterprise.serialization.test-util/with-world
metabase-enterprise.test/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.test/with-gtaps-for-user! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.advanced-permissions.api.util-test/with-impersonations! macros.metabase-enterprise.advanced-permissions.api.util-test/with-impersonations!
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
metabase.api.card-test/with-ordered-items macros.metabase.api.card-test/with-ordered-items
metabase.api.collection-test/with-collection-hierarchy! macros.metabase.api.collection-test/with-collection-hierarchy!
metabase.api.collection-test/with-some-children-of-collection! macros.metabase.api.collection-test/with-some-children-of-collection!
metabase.api.common/define-routes macros.metabase.api.common/define-routes
metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing! macros.metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing!
metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing! macros.metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing!
metabase.api.public-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.api.public-test/with-sharing-enabled-and-temp-card-referencing!
metabase.api.public-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.api.public-test/with-sharing-enabled-and-temp-dashcard-referencing!
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
metabase.lib.common/defop macros.metabase.lib.common/defop
metabase.models.params.chain-filter-test/chain-filter macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.params.chain-filter-test/chain-filter-search macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.query-analysis-test/with-test-setup macros.metabase.models.query-analysis-test/with-test-setup
metabase.models.user-test/with-groups macros.metabase.models.user-test/with-groups
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
metabase.related-test/with-world macros.metabase.related-test/with-world
metabase.shared.util.namespaces/import-fn macros.metabase.shared.util.namespaces/import-fn
metabase.task.setup.query-analysis-setup/with-test-setup! macros.metabase.task.setup.query-analysis-setup/with-test-setup!
metabase.test.data.users/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test.util/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test.util/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.test/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test/with-persistence-enabled! macros.metabase.test.persistence/with-persistence-enabled!
metabase.test/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.xrays.domain-entities.malli/define-getters-and-setters macros.metabase.xrays.domain-entities.malli/define-getters-and-setters}}
{clojurewerkz.quartzite.jobs/build macros.quartz/build-job
clojurewerkz.quartzite.schedule.cron/schedule macros.quartz/schedule
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.serialization.test-util/with-world macros.metabase-enterprise.serialization.test-util/with-world
metabase-enterprise.test/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!
metabase.api.card-test/with-ordered-items macros.metabase.api.card-test/with-ordered-items
metabase.api.collection-test/with-collection-hierarchy macros.metabase.api.collection-test/with-collection-hierarchy
metabase.api.collection-test/with-some-children-of-collection macros.metabase.api.collection-test/with-some-children-of-collection
metabase.api.common/defendpoint macros.metabase.api.common/defendpoint
metabase.api.common/defendpoint-async macros.metabase.api.common/defendpoint
metabase.api.common/defendpoint-async-schema macros.metabase.api.common/defendpoint
metabase.api.common/define-routes macros.metabase.api.common/define-routes
metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing macros.metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing
metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing macros.metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing
metabase.api.public-test/with-sharing-enabled-and-temp-card-referencing macros.metabase.api.public-test/with-sharing-enabled-and-temp-card-referencing
metabase.api.public-test/with-sharing-enabled-and-temp-dashcard-referencing macros.metabase.api.public-test/with-sharing-enabled-and-temp-dashcard-referencing
metabase.domain-entities.malli/define-getters-and-setters macros.metabase.domain-entities.malli/define-getters-and-setters
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
metabase.lib.common/defop macros.metabase.lib.common/defop
metabase.models.params.chain-filter-test/chain-filter macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.params.chain-filter-test/chain-filter-search macros.metabase.models.params.chain-filter-test/chain-filter
metabase.models.query-field-test/with-test-setup macros.metabase.models.query-field-test/with-test-setup
metabase.models.user-test/with-groups macros.metabase.models.user-test/with-groups
metabase.query-processor.streaming/streaming-response macros.metabase.query-processor.streaming/streaming-response
metabase.related-test/with-world macros.metabase.related-test/with-world
metabase.shared.util.namespaces/import-fn macros.metabase.shared.util.namespaces/import-fn
metabase.test.data.users/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test.util/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test.util/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values
metabase.test/with-group-for-user macros.metabase.test.data.users/with-group-for-user
metabase.test/with-persistence-enabled macros.metabase.test.persistence/with-persistence-enabled
metabase.test/with-temp-env-var-value! macros.metabase.test.util/with-temp-env-var-value!
metabase.test/with-temporary-raw-setting-values macros.metabase.test.util/with-temporary-raw-setting-values}}
:config-in-comment
{:linters {:unresolved-symbol {:level :off}}}
@@ -977,47 +620,35 @@
;; the following patterns are considered to be test namespaces:
;;
;; - Any namespace ending in `-test` or `-test.whatever`
;; - Any namespace containing in `test-util`
;; - Any namespace ending in `test-util`
;; - Any namespace that starts with `metabase.test`
;;
;; this list isn't exhaustive because it misses some stuff like the test runner and HTTP client but it's easier to go
;; fix those namespaces than it is to make this regex super hairy.
[{:pattern "(?:.*-test(?:\\..*)?$)|(?:.*test-util)|(?:^metabase\\.test.*)"
[{:pattern "(?:.*-test(?:\\..*)?$)|(?:.*test-util$)|(?:^metabase\\.test.*)"
:name test-namespaces}
{:pattern "^(?!.*-test(?:\\..*)?$)(?!.*test-util)(?!^metabase\\.test.*).*$"
{:pattern "^(?!.*-test(?:\\..*)?$)(?!.*test-util$)(?!^metabase\\.test.*).*$"
:name source-namespaces}
{:pattern "(?:(?:^metabase\\.cmd.*)|(?:^build.*)|(?:^metabuild-common.*)|(?:^release.*)|(?:^i18n.*)|(?:^lint-migrations-file$))"
:name printable-namespaces}
{:pattern "^metabase\\.(?:(?:lib)|(?:legacy-mbql))\\.*"
{:pattern "^metabase\\.lib\\.*"
:name metabase-lib}
;;
;; QP source namespaces: `metabase.query-processor.*`, `metabase-enterprise.sandbox.query-processor.*`, and
;; `metabase.driver.*`, excluding `-test` namespaces.
;;
{:pattern "metabase(?:(?:(?:-enterprise\\.sandbox)?\\.query-processor\\.)|(?:\\.driver\\.)).*(?<!-test)$"
:name qp-and-driver-source-namespaces}
;;
;; driver namespaces
;;
{:pattern "metabase\\.driver.*$"
:name driver-namespaces}
;;
;; general QP tests are ones that run across multiple drivers. Anything `metabase.query-processor*-test`,
;; `metabase.driver.sql*-test` (excluding SQLServer and SQLite), or
;; `metabase-enterprise.sandbox.query-processor*-test`
;;
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
:name general-qp-and-driver-test-namespaces}]
:name qp-and-driver-source-namespaces}]
:config-in-ns
{test-namespaces
{:linters
{:inline-def {:level :off}
:missing-docstring {:level :off}
:private-call {:level :off}
:metabase/defsetting-must-specify-export {:level :off}
:discouraged-var {metabase.driver/database-supports? {:level :off}}}
{:inline-def {:level :off}
:missing-docstring {:level :off}
:private-call {:level :off}
:metabase/defsetting-must-specify-export {:level :off}
:hooks.metabase.test.data/mbql-query-first-arg {:level :error}
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning}}
:hooks
{:analyze-call
{clojure.core/defmacro hooks.clojure.core/non-thread-safe-form-should-end-with-exclamation
@@ -1058,35 +689,19 @@
metabase.test/with-temp {:message "Don't use application database inside MLv2 code"}}
:discouraged-namespace
{metabase.driver {:message "MLv2 code should be independent of driver code/methods"}
metabase.test {:message "MLv2 tests should only use MLv2 test helpers"}
metabase.test.data {:message "MLv2 tests should only use MLv2 test helpers"}
metabase.util.malli.schema {:message "Prefer metabase.lib.schema schemas in MLv2 code"}
toucan.db {:message "Don't use application database inside MLv2 code"}
toucan.models {:message "Don't use application database inside MLv2 code"}
toucan.util.test {:message "Don't use application database inside MLv2 code"}
toucan2.core {:message "Don't use application database inside MLv2 code"}}}}
driver-namespaces
{:linters
{:discouraged-var
{metabase.driver/database-supports? {:level :off}}}}
{toucan.db {:message "Don't use application database inside MLv2 code"}
toucan.models {:message "Don't use application database inside MLv2 code"}
toucan.util.test {:message "Don't use application database inside MLv2 code"}
toucan2.core {:message "Don't use application database inside MLv2 code"}}}}
qp-and-driver-source-namespaces
{:linters
{:discouraged-namespace
{metabase.util.malli.schema {:message "Prefer metabase.lib.schema or metabase.legacy-mbql.schema schemas in QP/driver code"}
toucan.db {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
toucan.models {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
toucan.util.test {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
toucan2.core {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}
{toucan.db {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
toucan.models {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
toucan.util.test {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
toucan2.core {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}
:discouraged-var
{medley.core/map-keys {:message "Use clojure.core/update-keys"}
medley.core/map-vals {:message "Use clojure.core/update-vals"}
metabase.db.query/query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
metabase.db.query/reducible-query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}}}
general-qp-and-driver-test-namespaces
{:linters
{:metabase/disallow-hardcoded-driver-names-in-tests {:level :warning}}}}}
{metabase.db.query/query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}
metabase.db.query/reducible-query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}}}}}

View File

@@ -0,0 +1,204 @@
(ns hooks.clojure.core
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]))
(defn- node->qualified-symbol [node]
(try
(when (hooks/token-node? node)
(let [sexpr (hooks/sexpr node)]
(when (symbol? sexpr)
(let [resolved (hooks/resolve {:name sexpr})]
(when-not (= :clj-kondo/unknown-namespace (:ns resolved))
(symbol (name (:ns resolved)) (name (:name resolved))))))))
;; some symbols like `*count/Integer` aren't resolvable.
(catch Exception _
nil)))
(def ^:private white-card-symbols
'#{;; these toucan methods might actually set global values if it's used outside of a transaction,
;; but since mt/with-temp runs in a transaction, so we'll ignore them in this case.
toucan2.core/delete!
toucan2.core/update!
toucan2.core/insert!
toucan2.core/insert-returning-instance!
toucan2.core/insert-returning-instances!
toucan2.core/insert-returning-pk!
toucan2.core/insert-returning-pks!
clojure.core.async/<!!
clojure.core.async/>!!
clojure.core.async/alts!!
clojure.core.async/close!
clojure.core.async/poll!
clojure.core.memoize/memo-clear!
clojure.core/conj!
clojure.core/persistent!
clojure.core/reset!
clojure.core/swap!
clojure.core/volatile!
clojure.core/vreset!
clojure.core/vswap!
clojure.java.jdbc/execute!
methodical.core/add-aux-method-with-unique-key!
methodical.core/remove-aux-method-with-unique-key!
next.jdbc/execute!
;; Definitely thread safe
metabase.test.util.dynamic-redefs/patch-vars!
;; TODO: most of these symbols shouldn't be here, we should go through them and
;; find the functions/macros that use them and make sure their names end with !
;; best way to do this is try remove each of these and rely on kondo output to find places where it's used
clojure.test/grant-collection-perms!
clojure.test/grant-collection-perms-fn!
clojure.test/grant-perms-fn!
clojure.test/purge-old-entries!
clojure.test/revoke-collection-perms!
clojure.test/save-results!
metabase-enterprise.advanced-permissions.models.permissions/update-db-download-permissions!
metabase-enterprise.internal-user/install-internal-user!
metabase-enterprise.sso.integrations.saml-test/call-with-login-attributes-cleared!
metabase.actions/perform-action!
metabase.analytics.snowplow-test/fake-track-event-impl!
metabase.analytics.snowplow/track-event-impl!
metabase.api.public-test/add-card-to-dashboard!
metabase.cmd.dump-to-h2/dump-to-h2!
metabase.cmd.load-from-h2/load-from-h2!
metabase.core/ensure-audit-db-installed!
metabase.db.schema-migrations-test.impl/run-migrations-in-range!
metabase.db.setup/migrate!
metabase.db.setup/setup-db!
metabase.db/migrate!
metabase.db/setup-db!
metabase.driver.mongo-test/create-database-from-row-maps!
metabase.driver.postgres-test/create-enums-db!
metabase.driver.postgres-test/drop-if-exists-and-create-db!
metabase.driver.sql-jdbc.execute/execute-statement!
metabase.email-test/reset-inbox!
metabase.email/send-email!
metabase.models.action/insert!
metabase.models.collection.graph-test/clear-graph-revisions!
metabase.models.collection.graph-test/do-with-n-temp-users-with-personal-collections!
metabase.models.field-values/create-or-update-full-field-values!
metabase.models.model-index/add-values!
metabase.models.moderation-review/create-review!
metabase.models.on-demand-test/add-dashcard-with-parameter-mapping!
metabase.models.permissions/grant-application-permissions!
metabase.models.permissions/grant-collection-read-permissions!
metabase.models.permissions/grant-collection-readwrite-permissions!
metabase.models.permissions/grant-full-data-permissions!
metabase.models.permissions/grant-native-readwrite-permissions!
metabase.models.permissions/grant-permissions!
metabase.models.permissions/revoke-application-permissions!
metabase.models.permissions/revoke-data-perms!
metabase.models.permissions/update-data-perms-graph!
metabase.models.permissions/update-group-permissions!
metabase.models.persisted-info/ready-database!
metabase.models.revision/revert!
metabase.models.setting-test/test-user-local-allowed-setting!
metabase.models.setting-test/test-user-local-only-setting!
metabase.models.setting.cache/restore-cache!
metabase.models.setting/set!
metabase.models.setting/validate-settings-formatting!
metabase.permissions.test-util/with-restored-perms!
metabase.pulse/send-notifications!
metabase.pulse/send-pulse!
metabase.query-processor.streaming.interface/begin!
metabase.query-processor.streaming.interface/finish!
metabase.query-processor.streaming.interface/write-row!
metabase.sample-data/try-to-extract-sample-database!
metabase.setup/create-token!
metabase.sync.sync-metadata.fields.sync-metadata/update-field-metadata-if-needed!
metabase.sync.sync-metadata/sync-db-metadata!
metabase.sync.util-test/sync-database!
metabase.sync.util/store-sync-summary!
metabase.sync/sync-database!
metabase.task.index-values/job-init!
metabase.task.persist-refresh/job-init!
metabase.task.persist-refresh/refresh-tables!
metabase.task.persist-refresh/schedule-persistence-for-database!
metabase.task/delete-task!
metabase.test.data.bigquery-cloud-sdk/execute!
metabase.test.data.impl/copy-db-tables-and-fields!
metabase.test.data.impl/get-or-create-database!
metabase.test.data.impl/get-or-create-test-data-db!
metabase.test.data.interface/create-db!
metabase.test.data.interface/destroy-db!
metabase.test.data.oracle/create-user!
metabase.test.data.oracle/drop-user!
metabase.test.data.sql-jdbc.load-data/make-insert!
metabase.test.data.users/clear-cached-session-tokens!
metabase.test.initialize/do-initialization!
metabase.test.initialize/initialize-if-needed!
metabase.test.integrations.ldap/start-ldap-server!
metabase.test.util.log/ensure-unique-logger!
metabase.test.util.log/set-ns-log-level!
metabase.test.util/do-with-temp-env-var-value!
metabase.test.util/restore-raw-setting!
metabase.test.util/upsert-raw-setting!
metabase.test/initialize-if-needed!
metabase.test/test-helpers-set-global-values!
metabase.test/with-temp-env-var-value!
metabase.upload-test/set-local-infile!
metabase.util.files/create-dir-if-not-exists!
metabase.util.ssh-test/start-mock-servers!
metabase.util.ssh-test/stop-mock-servers!})
(defn- end-with-exclamation?
[s]
(str/ends-with? s "!"))
(defn- non-thread-safe-form-should-end-with-exclamation*
[{[defn-or-defmacro form-name] :children, :as node}]
(when-not (and (:string-value form-name)
(end-with-exclamation? (:string-value form-name)))
(letfn [(walk [f form]
(f form)
(doseq [child (:children form)]
(walk f child)))]
(walk (fn [form]
(when-let [qualified-symbol (node->qualified-symbol form)]
(when (and (not (contains? white-card-symbols qualified-symbol))
(end-with-exclamation? qualified-symbol))
(hooks/reg-finding! (assoc (meta form-name)
:message (format "The name of this %s should end with `!` because it contains calls to non thread safe form `%s`."
(:string-value defn-or-defmacro) qualified-symbol)
:type :metabase/test-helpers-use-non-thread-safe-functions)))))
node))
node))
(defn non-thread-safe-form-should-end-with-exclamation
"Used to ensure defn and defmacro in test namespace to have name ending with `!` if it's non-thread-safe.
A function or a macro can be defined as 'not thread safe' when their funciton name ends with a `!`.
Only used in tests to identify thread-safe/non-thread-safe test helpers. See #37126"
[{:keys [node cljc lang]}]
(when (or (not cljc)
(= lang :clj))
(non-thread-safe-form-should-end-with-exclamation* node))
{:node node})
(comment
(require '[clj-kondo.core :as clj-kondo])
(def form (str '(defmacro a
[x]
`(fun-call x))))
(def form "(defmacro a
[x]
`(some! ~x))")
(def form "(defun f
[x]
(let [g! (fn [] 1)]
(g!)))")
(str (hooks/parse-string form))
(hooks/sexpr (hooks/parse-string form))
(binding [hooks/*reload* true]
(-> form
(with-in-str (clj-kondo/run! {:lint ["-"]}))
:findings))
(do (non-thread-safe-form-should-end-with-exclamation* (hooks/parse-string form)) nil))

View File

@@ -0,0 +1,249 @@
(ns hooks.clojure.test
(:require [clj-kondo.hooks-api :as hooks]
[clojure.string :as str]))
(def ^:private disallowed-parallel-forms
"Things you should not be allowed to use inside parallel tests. Besides these, anything ending in `!` not whitelisted
in [[allowed-parallel-forms]] is not allowed."
'#{clojure.core/alter-var-root
clojure.core/with-redefs
clojure.core/with-redefs-fn
metabase-enterprise.sandbox.test-util/with-gtaps!
metabase-enterprise.sandbox.test-util/with-gtaps-for-user!
metabase-enterprise.sandbox.test-util/with-user-attributes
metabase-enterprise.test/with-gtaps!
metabase-enterprise.test/with-gtaps-for-user!
metabase-enterprise.test/with-user-attributes
metabase.actions.test-util/with-actions
metabase.actions.test-util/with-actions-disabled
metabase.actions.test-util/with-actions-enabled
metabase.actions.test-util/with-actions-test-data
metabase.actions.test-util/with-actions-test-data-and-actions-enabled
metabase.actions.test-util/with-actions-test-data-tables
metabase.email-test/with-expected-messages
metabase.email-test/with-fake-inbox
metabase.test.data.users/with-group
metabase.test.data.users/with-group-for-user
metabase.test.persistence/with-persistence-enabled
metabase.test.util.log/with-log-level
metabase.test.util.log/with-log-messages-for-level
metabase.test.util.misc/with-single-admin-user
metabase.test.util/with-all-users-permission
metabase.test.util/with-column-remappings
metabase.test.util/with-discarded-collections-perms-changes
metabase.test.util/with-env-keys-renamed-by
metabase.test.util/with-locale
metabase.test.util/with-non-admin-groups-no-root-collection-for-namespace-perms
metabase.test.util/with-non-admin-groups-no-root-collection-perms
metabase.test.util/with-temp-env-var-value
metabase.test.util/with-temp-vals-in-db
metabase.test.util/with-temporary-raw-setting-values
metabase.test.util/with-temporary-setting-values
metabase.test.util/with-user-in-groups
metabase.test/with-actions
metabase.test/with-actions-disabled
metabase.test/with-actions-enabled
metabase.test/with-actions-test-data
metabase.test/with-actions-test-data-and-actions-enabled
metabase.test/with-actions-test-data-tables
metabase.test/with-all-users-permission
metabase.test/with-column-remappings
metabase.test/with-discarded-collections-perms-changes
metabase.test/with-env-keys-renamed-by
metabase.test/with-expected-messages
metabase.test/with-fake-inbox
metabase.test/with-group
metabase.test/with-group-for-user
metabase.test/with-locale
metabase.test/with-log-level
metabase.test/with-log-messages-for-level
metabase.test/with-non-admin-groups-no-root-collection-for-namespace-perms
metabase.test/with-non-admin-groups-no-root-collection-perms
metabase.test/with-persistence-enabled
metabase.test/with-single-admin-user
metabase.test/with-temp-env-var-value
metabase.test/with-temp-vals-in-db
metabase.test/with-temporary-raw-setting-values
metabase.test/with-temporary-setting-values
metabase.test/with-user-in-groups})
;;; TODO -- we should disallow `metabase.test/user-http-request` with any method other than `:get`
(def ^:private allowed-parallel-forms
"These fns are destructive, but are probably fine inside ^:parallel tests because it usually means you're doing
something to an atom or something like that."
'#{clojure.core/assoc!
clojure.core/compare-and-set!
clojure.core/conj!
clojure.core/disj!
clojure.core/dissoc!
clojure.core/persistent!
clojure.core/pop!
clojure.core/reset!
clojure.core/reset-vals!
clojure.core/run!
clojure.core/swap!
clojure.core/swap-vals!
clojure.core/volatile!
clojure.core/vreset!
clojure.core/vswap!
clojure.core.async/<!
clojure.core.async/<!!
clojure.core.async/>!
clojure.core.async/>!!
clojure.core.async/alt!
clojure.core.async/alt!!
clojure.core.async/alts!
clojure.core.async/alts!!
clojure.core.async/close!
clojure.core.async/ioc-alts!
clojure.core.async/offer!
clojure.core.async/onto-chan!
clojure.core.async/onto-chan!!
clojure.core.async/poll!
clojure.core.async/put!
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
metabase.driver.sql-jdbc.execute/execute-prepared-statement!
metabase.query-processor.store/store-database!})
(defn- node->qualified-symbol [node]
(try
(when (hooks/token-node? node)
(let [sexpr (hooks/sexpr node)]
(when (symbol? sexpr)
(when-let [resolved (hooks/resolve {:name sexpr})]
(symbol (name (:ns resolved)) (name (:name resolved)))))))
;; some symbols like `*count/Integer` aren't resolvable.
(catch Exception _
nil)))
(defn- warn-about-disallowed-parallel-forms [form]
(letfn [(error! [form message]
(hooks/reg-finding! (assoc (meta form)
:message message
:type :metabase/validate-deftest)))
(f [form]
(when-let [qualified-symbol (node->qualified-symbol form)]
(cond
(disallowed-parallel-forms qualified-symbol)
(error! form (format "%s is not allowed inside a ^:parallel test or test fixture" qualified-symbol))
(and (not (allowed-parallel-forms qualified-symbol))
(str/ends-with? (name qualified-symbol) "!"))
(error! form (format "destructive functions like %s are not allowed inside a ^:parallel test or test fixture. If this should be allowed, add it to the whitelist in .clj-kondo/hooks/clojure/test.clj"
qualified-symbol)))))
(walk [form]
(f form)
(doseq [child (:children form)]
(walk child)))]
(walk form)))
(defn- deftest-check-parallel
"1. Check if test is marked ^:parallel / ^:synchronized correctly
2. Make sure disallowed forms are not used in ^:parallel tests"
[{[_ test-name & body] :children, :as _node}]
(let [test-metadata (:meta test-name)
metadata-sexprs (map hooks/sexpr test-metadata)
combined-metadata (transduce
(map (fn [x]
(if (map? x)
x
{x true})))
(completing merge)
{}
metadata-sexprs)
parallel? (:parallel combined-metadata)
synchronized? (:synchronized combined-metadata)]
(when (and parallel? synchronized?)
(hooks/reg-finding! (assoc (meta test-name)
:message "Test should not be marked both ^:parallel and ^:synchronized"
:type :metabase/validate-deftest)))
;; only when the custom `:metabase/deftest-not-marked-parallel` is enabled: complain if tests are not explicitly
;; marked `^:parallel` or `^:synchronized`. This is mostly to encourage people to mark everything `^:parallel` in
;; places like `metabase.lib` tests unless there is a really good reason not to.
(when-not (or parallel? synchronized?)
(hooks/reg-finding!
(assoc (meta test-name)
:message "Test should be marked either ^:parallel or ^:synchronized"
:type :metabase/deftest-not-marked-parallel-or-synchronized)))
(when parallel?
(doseq [form body]
(warn-about-disallowed-parallel-forms form)))))
(def ^:private number-of-lines-for-a-test-to-be-considered-horrifically-long
200)
(defn- deftest-check-not-horrifically-long
[node]
(let [{:keys [row end-row]} (meta node)]
(when (and row end-row)
(let [num-lines (- end-row row)]
(when (>= num-lines number-of-lines-for-a-test-to-be-considered-horrifically-long)
(hooks/reg-finding! (assoc (meta node)
:message (str (format "This test is horrifically long, it's %d lines! 😱 " num-lines)
"Do you really want to try to debug it if it fails? 💀 "
"Split it up into smaller tests! 🥰")
:type :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests)))))))
(defn deftest [{:keys [node cljc lang]}]
;; run [[deftest-check-parallel]] only once... if this is a `.cljc` file only run it for the `:clj` analysis, no point
;; in running it twice.
(when (or (not cljc)
(= lang :clj))
(deftest-check-parallel node))
(deftest-check-not-horrifically-long node)
{:node node})
;;; this is a hacky way to determine whether these namespaces are required in the `ns` form or not... basically `:ns`
;;; will come back as `nil` if they are not.
(defn- approximately-equal-ns-required? []
(= (:ns (hooks/resolve {:name 'metabase.test-runner.assert-exprs.approximately-equal/=?-report}))
'metabase.test-runner.assert-exprs.approximately-equal))
(defn- malli-equals-ns-required? []
(= (:ns (hooks/resolve {:name 'metabase.test-runner.assert-exprs.malli-equals/malli=-report}))
'metabase.test-runner.assert-exprs.malli-equals))
(defn- warn-about-missing-test-expr-requires-in-cljs [{:keys [children], :as _is-node}]
(let [[_is assertion-node] children]
(when (hooks/list-node? assertion-node)
(let [[assertion-symb-node] (:children assertion-node)]
(when (hooks/token-node? assertion-symb-node)
(let [assertion-symb-token (hooks/sexpr assertion-symb-node)
warn! (fn [ns-to-require]
(hooks/reg-finding!
(assoc (meta assertion-symb-node)
:message (format "You must require %s to use %s in ClojureScript"
ns-to-require
assertion-symb-token)
:type :metabase/missing-test-expr-requires-in-cljs)))]
(condp = assertion-symb-token
'=?
(when-not (approximately-equal-ns-required?)
(warn! 'metabase.test-runner.assert-exprs.approximately-equal))
'malli=
(when-not (malli-equals-ns-required?)
(warn! 'metabase.test-runner.assert-exprs.malli-equals))
nil)))))))
(defn- warn-about-schema= [{[_is assertion-node] :children, :as _is-node}]
(let [{[assertion-symb-node] :children} assertion-node]
(when (and (hooks/token-node? assertion-symb-node)
(= (hooks/sexpr assertion-symb-node) 'schema=))
(hooks/reg-finding! (assoc (meta assertion-symb-node)
:message "Use =? or malli= instead of schema="
:type :metabase/warn-about-schema=)))))
(defn is [{:keys [node lang]}]
(when (= lang :cljs)
(warn-about-missing-test-expr-requires-in-cljs node))
(warn-about-schema= node)
{:node node})
(defn use-fixtures [{:keys [node]}]
(warn-about-disallowed-parallel-forms node)
{:node node})

371
.clj-kondo/hooks/common.clj Normal file
View File

@@ -0,0 +1,371 @@
(ns hooks.common
(:require [clj-kondo.hooks-api :as hooks]
[clojure.pprint]))
(defn with-macro-meta
"When introducing internal nodes (let, defn, etc) it is important to provide a meta of an existing token
as the current version of kondo will use the whole form by default."
[new-node hook-node]
(with-meta new-node (meta (first (:children hook-node)))))
;;; This stuff is to help debug hooks. Trace a function and it will pretty-print the before & after sexprs.
;;;
;;; (hooks.common/trace #'calculate-bird-scarcity)
(defn- trace* [f]
{:pre [(fn? f)]}
(fn traced-fn [node]
(println \newline)
(clojure.pprint/pprint (hooks/sexpr (:node node)))
(let [node* (f node)]
(println '=>)
(clojure.pprint/pprint (hooks/sexpr (:node node*)))
node*)))
;;; this doesn't seem to work with SCI. Use [[defn-traced]] instead.
;; (defn trace [varr]
;; {:pre [(var? varr)]}
;; (when-not (::traced? (meta varr))
;; (alter-var-root varr trace*)
;; (alter-meta! varr assoc ::traced? true)
;; (println "Traced" varr)))
(defmacro defn-traced
"Replace a hook [[defn]] with this to trace the before and after sexprs."
[fn-name & fn-tail]
`(do
(defn fn# ~@fn-tail)
(def ~fn-name (trace* fn#))))
;;;; Common hook definitions
(defn do*
"This is the same idea as [[clojure.core/do]] but doesn't cause Kondo to complain about redundant dos or unused values."
[{{[_ & args] :children, :as node} :node}]
(let [node* (-> (hooks/list-node
(list*
(with-meta (hooks/token-node 'do) {:clj-kondo/ignore [:redundant-do]})
(for [arg args]
(vary-meta arg update :clj-kondo/ignore #(conj (vec %) :unused-value)))))
(with-meta (meta node)))]
{:node node*}))
(defn with-one-binding
"Helper for macros that have a shape like
(my-macro [x]
...)
=>
(let [x nil]
...)
Binding is optional and `_` will be substituted if not supplied."
[{{[_ {[x] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-two-bindings
"Helper for macros that have a shape like
(my-macro [x y]
...)
=>
(let [x nil, y nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[x y] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
(or y (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-three-bindings
"Helper for macros that have a shape like
(my-macro [x y z]
...)
=>
(let [x nil, y nil, z nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[x y z] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
(or y (hooks/token-node '_)) (hooks/token-node 'nil)
(or z (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-four-bindings
"Helper for macros that have a shape like
(my-macro [a b c d]
...)
=>
(let [a nil, b nil, c nil, d nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-five-bindings
"Helper for macros that have a shape like
(my-macro [a b c d e]
...)
=>
(let [a nil, b nil, c nil, d nil, e nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d e] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
(or e (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-six-bindings
"Helper for macros that have a shape like
(my-macro [a b c d e f]
...)
=>
(let [a nil, b nil, c nil, d nil, e nil, f nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d e f] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
(or e (hooks/token-node '_)) (hooks/token-node 'nil)
(or f (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-seven-bindings
"Helper for macros that have a shape like
(my-macro [a b c d e f g]
...)
=>
(let [a nil, b nil, c nil, d nil, e nil, f nil, g nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d e f g] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
(or e (hooks/token-node '_)) (hooks/token-node 'nil)
(or f (hooks/token-node '_)) (hooks/token-node 'nil)
(or g (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-one-top-level-binding
"Helper for macros that have a shape like
(my-macro x
...)
=>
(let [x nil]
...)"
[{{[_ x & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[x (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-two-top-level-bindings
"Helper for macros that have a shape like
(my-macro x y
...)
=>
(let [x nil, y nil]
...)"
[{{[_ x y & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[x (hooks/token-node 'nil)
y (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn let-one-with-optional-value
"This is exactly like [[clojure.core/let]] but the right-hand side of the binding, `value`, is optional, and only one
binding can be established.
(some-macro [x] ...)
=>
(let [x nil] ...)
(some-macro [x 100] ...)
=>
(let [x 100] ...)"
[{{[_ {[binding value] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[binding (or value (hooks/token-node 'nil))])
body))]
{:node node*}))
(defn- let-second-inner [body bindings]
(let [binding-infos (for [[model {[binding value] :children}] (partition 2 bindings)]
{:model model
:binding binding
:value (or value
(hooks/token-node 'nil))})]
(-> (hooks/vector-node
[(hooks/vector-node (map :model binding-infos))
(-> (hooks/list-node (list* (hooks/token-node `let)
(hooks/vector-node (mapcat (juxt :binding :value) binding-infos))
body))
(with-meta (meta body)))])
(with-meta (meta body)))))
(defn let-second
"Helper for macros that have a shape like
(my-macro x [y]
...)
where the let is for second arg.
=>
(let [y nil]
...)"
[{:keys [node]}]
(let [[_ first-arg-ref binding+opts & body] (:children node)]
{:node (let-second-inner body [first-arg-ref binding+opts])}))
(defn let-with-optional-value-for-last-binding
"This is exactly like [[clojure.core/let]] but the right-hand side of the *last* binding, `value`, is optional.
(some-macro [x] ...)
=>
(let [x nil] ...)
(some-macro [x 100] ...)
=>
(let [x 100] ...)"
[{{[_ {bindings :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
(into []
(comp (partition-all 2)
(mapcat (fn [[binding value]]
[binding (or value (hooks/token-node 'nil))])))
bindings))
body))]
{:node node*}))
(defn with-ignored-first-arg
"For macros like
(discard-setting-changes [setting-1 setting-2]
...)
=>
(do ...)
where the first arg ought to be ignored for linting purposes."
[{{[_ _x & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'do)
body))]
{:node node*}))
(defn with-used-first-arg
"For macros like
(with-drivers (filter pred? some-drivers)
...)
=>
(let [_1234 (filter pred? some-drivers)]
...)
where the first arg should be linted and appear to be used."
[{{[_ arg & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node [(hooks/token-node (gensym "_"))
arg])
body))]
{:node node*}))

View File

@@ -4,8 +4,8 @@
(comment
(def input (hooks/parse-string
"(with-indexed-model! [{:keys [model model-index model-index-value]}
{:query :some-query}]
"(with-indexed-model [{:keys [model model-index model-index-value]}
{:query :some-query}]
:foo :bar :body)"))
(println (hooks/sexpr input))
@@ -13,12 +13,12 @@
(let [f (load-file ".clj-kondo/hooks/metabase/api/automagic_dashboards_test.clj")]
(f {:node input})))
(defn with-indexed-model!
(defn with-indexed-model
[{node :node}]
(let [[_macro-call binding-and-query-info & body] (:children node)
[binding query-info] (:children binding-and-query-info)
node* (hooks/list-node
[(hooks/token-node 'do-with-indexed-model!)
[(hooks/token-node 'do-with-testing-model)
query-info
(hooks/list-node
(list* (hooks/token-node 'fn)

View File

@@ -1,7 +1,5 @@
(ns hooks.metabase.models.interface
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
(:require [clj-kondo.hooks-api :as hooks]))
(defn define-hydration-method
"This is used for both [[metabase.models.interface/define-simple-hydration-method]] and
@@ -16,6 +14,5 @@
(hooks/token-node 'defn)
fn-name
fn-tail))))
(with-meta (meta node))
hooks.common/add-lsp-ignore-unused-public-var-metadata)]
(with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))]
{:node node}))

View File

@@ -38,6 +38,8 @@
engines
enum-cardinality-threshold
follow-up-email-sent
ga-code
ga-enabled
google-auth-auto-create-accounts-domain
google-auth-client-id
google-auth-configured
@@ -90,8 +92,6 @@
metabot-get-prompt-templates-url
metabot-prompt-generator-token-limit
multi-setting-read-only
notebook-native-preview-shown
notebook-native-preview-sidebar-width
notification-link-base-url
num-metabot-choices
openai-api-key
@@ -162,6 +162,7 @@
token-status
toucan-name
uncached-setting
uploads-table-prefix
user-visibility
version
version-info
@@ -181,13 +182,12 @@
setter-node (-> (list
(hooks/token-node 'defn)
(with-meta
(hooks/token-node (symbol (str (hooks/sexpr setting-name) \!)))
(meta setting-name))
(hooks/token-node (symbol (str (hooks/sexpr setting-name) \!)))
(meta setting-name))
(hooks/string-node "Docstring.")
(hooks/vector-node [(hooks/token-node '_value-or-nil)]))
hooks/list-node
(with-meta (meta node))
common/add-lsp-ignore-unused-public-var-metadata)]
(with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))]
(when (nil? (second (drop-while (comp not #{[:k :export?]} first) options-list)))
(when-not (contains? ignored-implicit-export? (:value setting-name))
@@ -243,14 +243,14 @@
(comment
(defn- defsetting* [form]
(hooks/sexpr
(:node
(defsetting
{:node
(hooks/parse-string
(with-out-str
#_{:clj-kondo/ignore [:unresolved-namespace]}
(clojure.pprint/pprint
form)))}))))
(:node
(defsetting
{:node
(hooks/parse-string
(with-out-str
#_{:clj-kondo/ignore [:unresolved-namespace]}
(clojure.pprint/pprint
form)))}))))
(defn x []
(defsetting*

View File

@@ -1,7 +1,6 @@
(ns hooks.metabase.public-settings.premium-features
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
[clj-kondo.hooks-api :as hooks]))
(defn defenterprise [{node :node}]
(let [[_defenterprise fn-name & args] (:children node)
@@ -27,8 +26,7 @@
fn-name
docstring))
fn-tail))
(with-meta (meta node))
hooks.common/add-lsp-ignore-unused-public-var-metadata)))}))
(with-meta (update (meta node) :clj-kondo/ignore #(hooks/vector-node (cons :clojure-lsp/unused-public-var (:children %))))))))}))
(comment
(defn- defenterprise* [form]

View File

@@ -11,10 +11,6 @@
[]
(not-empty (set (keys (:clj (hooks/ns-analysis 'metabase.test.data.dataset-definitions))))))
(defn- local-def?
[ns* sexpr]
(= ns* (:ns (hooks/resolve {:name sexpr}))))
(defn- dataset-type
"`dataset` can be one of:
@@ -25,7 +21,7 @@
We can only determine if an unqualified symbol refers to something in the dataset definitions namespace if there are
cached results available from [[global-dataset-symbols]]."
[this-ns dataset]
[dataset]
(let [sexpr (hooks/sexpr dataset)
global-defs (global-dataset-symbols)]
(cond
@@ -35,19 +31,20 @@
(namespace sexpr)
:qualified
(empty? global-defs)
:unqualified/unknown
(contains? global-defs sexpr)
:unqualified/from-dataset-defs-namespace
(local-def? this-ns sexpr)
:unqualified/local-def
;; either something defined in the current namespace or let-bound in the current scope.
:else
:unqualified/unknown)))
:unqualified/local-def)))
(defn dataset
[{{[_ dataset & body] :children} :node this-ns :ns}]
[{{[_ dataset & body] :children} :node}]
(let [noop (constantly nil)
body (case (dataset-type this-ns dataset)
body (case (dataset-type dataset)
;; non-symbol, qualified symbols, and unqualified symbols from the current namespace/let-bound can all
;; get converted from something like
;;
@@ -142,7 +139,7 @@
(when-not ((some-fn symbol? nil?) (hooks/sexpr table))
(hooks/reg-finding! (assoc (meta table)
:message "First arg to mbql-query should be either a table name symbol or nil."
:type :metabase/mbql-query-first-arg)))
:type ::mbql-query-first-arg)))
(let [result (replace-$id-special-tokens query)
;; HACK I'm not sure WHY it works but I ran into https://github.com/clj-kondo/clj-kondo/issues/1773 when
;; trying to get this working -- for some magical reason wrapping the whole thing in a `do` form seems to fix

View File

@@ -1,7 +1,7 @@
(ns macros.metabase.api.collection-test
(:require [macros.common]))
(defmacro with-collection-hierarchy! [bindings & body]
(defmacro with-collection-hierarchy [bindings & body]
`(let [~(macros.common/ignore-unused 'a) nil
~(macros.common/ignore-unused 'b) nil
~(macros.common/ignore-unused 'c) nil
@@ -12,6 +12,6 @@
~bindings
~@body))
(defmacro with-some-children-of-collection! [collection-or-id-or-nil & body]
(defmacro with-some-children-of-collection [collection-or-id-or-nil & body]
`(let [~(macros.common/ignore-unused '&ids) ~collection-or-id-or-nil]
~@body))

View File

@@ -1,7 +1,26 @@
(ns macros.metabase.api.common)
(ns macros.metabase.api.common
(:require
[clojure.string :as str]))
(defmacro define-routes
"Macro for api.common/define-routes"
[& args]
`(do (def ~'routes "docstring" nil)
~@args))
(defn route-fn-name
"route fn hook"
[method route]
(let [route (if (vector? route) (first route) route)]
(-> (str (name method) route)
(str/replace #"/" "_")
symbol)))
(defmacro defendpoint
"Macro for api.common/defendpoint*"
[method route & body]
(let [method-fn (symbol "compojure.core" (str method))
route-name (route-fn-name method route)]
`(do
~method-fn
(defn ~route-name ~@body))))

View File

@@ -1,11 +1,11 @@
(ns macros.metabase.api.embed-test)
(defmacro with-embedding-enabled-and-temp-card-referencing!
(defmacro with-embedding-enabled-and-temp-card-referencing
[table-kw field-kw [card-binding] & body]
`(let [~card-binding [~table-kw ~field-kw]]
~@body))
(defmacro with-embedding-enabled-and-temp-dashcard-referencing!
(defmacro with-embedding-enabled-and-temp-dashcard-referencing
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
`(let [~dash-binding [~table-kw ~field-kw]
~(or card-binding '_) nil

View File

@@ -1,11 +1,11 @@
(ns macros.metabase.api.public-test)
(defmacro with-sharing-enabled-and-temp-card-referencing!
(defmacro with-sharing-enabled-and-temp-card-referencing
[table-kw field-kw [card-binding] & body]
`(let [~card-binding [~table-kw ~field-kw]]
~@body))
(defmacro with-sharing-enabled-and-temp-dashcard-referencing!
(defmacro with-sharing-enabled-and-temp-dashcard-referencing
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
`(let [~dash-binding [~table-kw ~field-kw]
~(or card-binding '_) nil

View File

@@ -0,0 +1,16 @@
(ns macros.metabase.domain-entities.malli)
(defmacro define-getters-and-setters [schema & defs]
`(do
(comment ~schema) ;; Reference the schema
~@(for [[sym _path] (partition 2 defs)]
`(do
(defn ~(vary-meta sym
assoc :export true)
"docs" [_x#] nil)
(defn ~(vary-meta (symbol (str "with-" (name sym)))
assoc :export true)
"docs" [x# _new#] x#)
(defn ~(vary-meta (symbol (str (name sym) "-js"))
assoc :export true)
"docs" [x#] x#)))))

View File

@@ -1,9 +0,0 @@
(ns macros.metabase.models.query-analysis-test
(:require [macros.common]))
(defmacro with-test-setup [& body]
`(let [~(macros.common/ignore-unused 'card-id) 1
~(macros.common/ignore-unused 'table-id) 2
~(macros.common/ignore-unused 'tax-id) 3
~(macros.common/ignore-unused 'total-id) 4]
~@body))

View File

@@ -0,0 +1,9 @@
(ns macros.metabase.models.query-field-test
(:require [macros.common]))
(defmacro with-test-setup [& body]
`(let [~(macros.common/ignore-unused 'card-id) 1
~(macros.common/ignore-unused 'table-id) 2
~(macros.common/ignore-unused 'tax-id) 3
~(macros.common/ignore-unused 'total-id) 4]
~@body))

View File

@@ -1,13 +0,0 @@
(ns macros.metabase.task.setup.query-analysis-setup
(:require [macros.common]))
(defmacro with-test-setup!
"Set up the data required to test the Query Analysis related tasks"
[& body]
`(let [~(macros.common/ignore-unused 'c1) 1
~(macros.common/ignore-unused 'c2) 2
~(macros.common/ignore-unused 'c3) 3
~(macros.common/ignore-unused 'c4) 4
~(macros.common/ignore-unused 'archived) 5
~(macros.common/ignore-unused 'invalid) 6]
~@body))

View File

@@ -1,5 +1,5 @@
(ns macros.metabase.test.persistence)
(defmacro with-persistence-enabled! [[persist-fn-sym] & body]
(defmacro with-persistence-enabled [[persist-fn-sym] & body]
`(clojure.core/let [~persist-fn-sym (fn [])]
~@body))

View File

@@ -1,16 +0,0 @@
(ns macros.metabase.xrays.domain-entities.malli)
(defmacro define-getters-and-setters [schema & defs]
`(do
(comment ~schema) ;; Reference the schema
~@(for [[sym _path] (partition 2 defs)]
`(do
(defn ~(vary-meta sym
assoc :export true)
"docs" [_x#] nil)
(defn ~(vary-meta (symbol (str "with-" (name sym)))
assoc :export true)
"docs" [x# _new#] x#)
(defn ~(vary-meta (symbol (str (name sym) "-js"))
assoc :export true)
"docs" [x#] x#)))))

View File

@@ -1,6 +0,0 @@
(ns macros.metabase-enterprise.advanced-permissions.api.util-test
(:require [macros.common]))
(defmacro with-impersonations! [impersonations-and-attributes-map & body]
`(let [~(macros.common/ignore-unused '&group) ~impersonations-and-attributes-map]
~@body))

View File

@@ -1,12 +0,0 @@
(ns macros.metabase-enterprise.query-reference-validation.api-test
(:require [macros.common]))
(defmacro with-test-setup! [& body]
`(let [~(macros.common/ignore-unused 'card-1) 1
~(macros.common/ignore-unused 'card-2) 2
~(macros.common/ignore-unused 'card-3) 3
~(macros.common/ignore-unused 'card-4) 4
~(macros.common/ignore-unused 'card-5) 5
~(macros.common/ignore-unused 'coll-2) 100
~(macros.common/ignore-unused 'coll-3) 200]
~@body))

View File

@@ -1,320 +0,0 @@
(ns hooks.clojure.core
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]
[hooks.common]))
;;; TODO -- seems silly to maintain different blacklists and whitelists here than we use for the deftest `^:parallel`
;;; checker... those lists live in the Clj Kondo config file
(def ^:private symbols-allowed-in-fns-not-ending-in-an-exclamation-point
'#{;; these toucan methods might actually set global values if it's used outside of a transaction,
;; but since mt/with-temp runs in a transaction, so we'll ignore them in this case.
toucan2.core/delete!
toucan2.core/update!
toucan2.core/insert!
toucan2.core/insert-returning-instance!
toucan2.core/insert-returning-instances!
toucan2.core/insert-returning-pk!
toucan2.core/insert-returning-pks!
clojure.core.async/<!!
clojure.core.async/>!!
clojure.core.async/alts!!
clojure.core.async/close!
clojure.core.async/poll!
clojure.core.memoize/memo-clear!
clojure.core/conj!
clojure.core/persistent!
clojure.core/reset!
clojure.core/swap!
clojure.core/volatile!
clojure.core/vreset!
clojure.core/vswap!
clojure.java.jdbc/execute!
methodical.core/add-aux-method-with-unique-key!
methodical.core/remove-aux-method-with-unique-key!
next.jdbc/execute!
;; Definitely thread safe
metabase.test.util.dynamic-redefs/patch-vars!
;; TODO: most of these symbols shouldn't be here, we should go through them and
;; find the functions/macros that use them and make sure their names end with !
;; best way to do this is try remove each of these and rely on kondo output to find places where it's used
clojure.test/grant-collection-perms!
clojure.test/grant-collection-perms-fn!
clojure.test/grant-perms-fn!
clojure.test/purge-old-entries!
clojure.test/revoke-collection-perms!
clojure.test/save-results!
metabase-enterprise.advanced-permissions.models.permissions/update-db-download-permissions!
metabase-enterprise.internal-user/install-internal-user!
metabase-enterprise.sso.integrations.saml-test/call-with-login-attributes-cleared!
metabase.actions/perform-action!
metabase.analytics.snowplow-test/fake-track-event-impl!
metabase.analytics.snowplow/track-event-impl!
metabase.api.public-test/add-card-to-dashboard!
metabase.cmd.dump-to-h2/dump-to-h2!
metabase.cmd.load-from-h2/load-from-h2!
metabase.core/ensure-audit-db-installed!
metabase.db.schema-migrations-test.impl/run-migrations-in-range!
metabase.db.setup/migrate!
metabase.db.setup/setup-db!
metabase.db/migrate!
metabase.db/setup-db!
metabase.driver.mongo-test/create-database-from-row-maps!
metabase.driver.postgres-test/create-enums-db!
metabase.driver.postgres-test/drop-if-exists-and-create-db!
metabase.driver.sql-jdbc.execute/execute-statement!
metabase.email-test/reset-inbox!
metabase.email/send-email!
metabase.models.action/insert!
metabase.models.collection.graph-test/clear-graph-revisions!
metabase.models.collection.graph-test/do-with-n-temp-users-with-personal-collections!
metabase.models.field-values/create-or-update-full-field-values!
metabase.models.model-index/add-values!
metabase.models.moderation-review/create-review!
metabase.models.on-demand-test/add-dashcard-with-parameter-mapping!
metabase.models.permissions/grant-application-permissions!
metabase.models.permissions/grant-collection-read-permissions!
metabase.models.permissions/grant-collection-readwrite-permissions!
metabase.models.permissions/grant-full-data-permissions!
metabase.models.permissions/grant-native-readwrite-permissions!
metabase.models.permissions/grant-permissions!
metabase.models.permissions/revoke-application-permissions!
metabase.models.permissions/revoke-data-perms!
metabase.models.permissions/update-data-perms-graph!
metabase.models.permissions/update-group-permissions!
metabase.models.persisted-info/ready-database!
metabase.models.revision/revert!
metabase.models.setting-test/test-user-local-allowed-setting!
metabase.models.setting-test/test-user-local-only-setting!
metabase.models.setting.cache/restore-cache!
metabase.models.setting/set!
metabase.models.setting/validate-settings-formatting!
metabase.permissions.test-util/with-restored-perms!
metabase.pulse/send-notifications!
metabase.pulse/send-pulse!
metabase.query-processor.streaming.interface/begin!
metabase.query-processor.streaming.interface/finish!
metabase.query-processor.streaming.interface/write-row!
metabase.sample-data/try-to-extract-sample-database!
metabase.setup/create-token!
metabase.sync.sync-metadata.fields.sync-metadata/update-field-metadata-if-needed!
metabase.sync.sync-metadata/sync-db-metadata!
metabase.sync.util-test/sync-database!
metabase.sync.util/store-sync-summary!
metabase.sync/sync-database!
metabase.task.index-values/job-init!
metabase.task.persist-refresh/job-init!
metabase.task.persist-refresh/refresh-tables!
metabase.task.persist-refresh/schedule-persistence-for-database!
metabase.task/delete-task!
metabase.test.data.bigquery-cloud-sdk/execute!
metabase.test.data.impl/copy-db-tables-and-fields!
metabase.test.data.impl/get-or-create-database!
metabase.test.data.impl/get-or-create-default-dataset!
metabase.test.data.impl.get-or-create/set-test-db-permissions!
metabase.test.data.interface/create-db!
metabase.test.data.interface/destroy-db!
metabase.test.data.oracle/create-user!
metabase.test.data.oracle/drop-user!
metabase.test.data.sql-jdbc.load-data/make-insert!
metabase.test.data.users/clear-cached-session-tokens!
metabase.test.initialize/do-initialization!
metabase.test.initialize/initialize-if-needed!
metabase.test.integrations.ldap/start-ldap-server!
metabase.test.util.log/ensure-unique-logger!
metabase.test.util.log/set-ns-log-level!
metabase.test.util/do-with-temp-env-var-value!
metabase.test.util/restore-raw-setting!
metabase.test.util/upsert-raw-setting!
metabase.test/initialize-if-needed!
metabase.test/test-helpers-set-global-values!
metabase.test/with-temp-env-var-value!
metabase.upload-test/set-local-infile!
metabase.util.files/create-dir-if-not-exists!
metabase.util.ssh-test/start-mock-servers!
metabase.util.ssh-test/stop-mock-servers!})
(defn- end-with-exclamation?
[s]
(str/ends-with? s "!"))
(defn- non-thread-safe-form-should-end-with-exclamation*
[{[defn-or-defmacro form-name] :children, :as node} config]
(when-not (and (:string-value form-name)
(end-with-exclamation? (:string-value form-name)))
(letfn [(walk [f form]
(f form)
(doseq [child (:children form)]
(walk f child)))]
(walk (fn [form]
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
(when (and (not (contains? symbols-allowed-in-fns-not-ending-in-an-exclamation-point qualified-symbol))
(or (end-with-exclamation? qualified-symbol)
(contains? (get-in config [:linters :metabase/validate-deftest :parallel/unsafe]) qualified-symbol)))
(hooks/reg-finding! (assoc (meta form-name)
:message (format "The name of this %s should end with `!` because it contains calls to non thread safe form `%s`. [:metabase/test-helpers-use-non-thread-safe-functions]"
(:string-value defn-or-defmacro) qualified-symbol)
:type :metabase/test-helpers-use-non-thread-safe-functions)))))
node))
node))
(defn non-thread-safe-form-should-end-with-exclamation
"Used to ensure defn and defmacro in test namespace to have name ending with `!` if it's non-thread-safe.
A function or a macro can be defined as 'not thread safe' when their funciton name ends with a `!`.
Only used in tests to identify thread-safe/non-thread-safe test helpers. See #37126"
[{:keys [node cljc lang config]}]
(when (or (not cljc)
(= lang :clj))
(non-thread-safe-form-should-end-with-exclamation* node config))
{:node node})
(comment
(require '[clj-kondo.core :as clj-kondo])
(def form (str '(defmacro a
[x]
`(fun-call x))))
(def form "(defmacro a
[x]
`(some! ~x))")
(def form "(defun f
[x]
(let [g! (fn [] 1)]
(g!)))")
(str (hooks/parse-string form))
(hooks/sexpr (hooks/parse-string form))
(binding [hooks/*reload* true]
(-> form
(with-in-str (clj-kondo/run! {:lint ["-"]}))
:findings))
(do (non-thread-safe-form-should-end-with-exclamation* (hooks/parse-string form)) nil))
(defn- ns-form-node->require-node [ns-form-node]
(some (fn [node]
(when (and (hooks/list-node? node)
(let [first-child (first (:children node))]
(and (hooks/keyword-node? first-child)
(= (hooks/sexpr first-child) :require))))
node))
(:children ns-form-node)))
(defn- lint-require-shapes [ns-form-node]
(doseq [node (-> ns-form-node
ns-form-node->require-node
:children
rest)]
(cond
(not (hooks/vector-node? node))
(hooks/reg-finding! (assoc (meta node)
:message "All :required namespaces should be wrapped in vectors [:metabase/require-shape-checker]"
:type :metabase/require-shape-checker))
(hooks/vector-node? (second (:children node)))
(hooks/reg-finding! (assoc (meta node)
:message "Don't use prefix forms inside :require [:metabase/require-shape-checker]"
:type :metabase/require-shape-checker)))))
(defn- require-node->namespace-symb-nodes [require-node]
(let [[_ns & args] (:children require-node)]
(into []
;; prefixed namespace forms are NOT SUPPORTED!!!!!!!!1
(keep (fn [node]
(cond
(hooks/vector-node? node)
;; propagate the metadata attached to this vector in case there's a `:clj-kondo/ignore` form.
(vary-meta (first (:children node)) (partial merge (meta require-node) (meta node)))
;; this should also be dead code since we require requires to be vectors
(hooks/token-node? node)
(vary-meta node (partial merge (meta require-node)))
:else
(printf "Don't know how to figure out what namespace is being required in %s\n" (pr-str node)))))
args)))
(defn- ns-form-node->ns-symb [ns-form-node]
(some-> (some (fn [node]
(when (and (hooks/token-node? node)
(not= (hooks/sexpr node) 'ns))
node))
(:children ns-form-node))
hooks/sexpr))
(defn- module
"E.g.
(module 'metabase.qp.middleware.wow) => 'metabase.qp"
[ns-symb]
(some-> (re-find #"^metabase\.[^.]+" (str ns-symb)) symbol))
(defn- ignored-namespace? [ns-symb config]
(some
(fn [pattern-str]
(re-find (re-pattern pattern-str) (str ns-symb)))
(:ignored-namespace-patterns config)))
(defn- module-api-namespaces
"Set API namespaces for a given module. `:any` means you can use anything, there are no API namespaces for this
module (yet). If unspecified, the default is just the namespace with the same name as the module e.g.
`metabase.db`."
[module config]
(let [module-config (get-in config [:api-namespaces module])]
(cond
(= module-config :any)
nil
(set? module-config)
module-config
:else
#{module})))
(defn- lint-modules [ns-form-node config]
(let [ns-symb (ns-form-node->ns-symb ns-form-node)]
(when-not (ignored-namespace? ns-symb config)
(when-let [current-module (module ns-symb)]
(let [allowed-modules (get-in config [:allowed-modules current-module])
required-namespace-symb-nodes (-> ns-form-node
ns-form-node->require-node
require-node->namespace-symb-nodes)]
(doseq [node required-namespace-symb-nodes
:let [clj-kondo-ignore (some-> (meta node) :clj-kondo/ignore hooks/sexpr set)]
:when (not (contains? clj-kondo-ignore :metabase/ns-module-checker))
:let [required-namespace (hooks/sexpr node)
required-module (module required-namespace)]
;; ignore stuff not in a module i.e. non-Metabase stuff.
:when required-module
:let [in-current-module? (= required-module current-module)]
:when (not in-current-module?)
:let [allowed-module? (or (= allowed-modules :any)
(contains? (set allowed-modules) required-module))
module-api-namespaces (module-api-namespaces required-module config)
allowed-module-namespace? (or (empty? module-api-namespaces)
(contains? module-api-namespaces required-namespace))]]
(when-let [error (cond
(not allowed-module?)
(format "Module %s should not be used in the %s module. [:metabase/ns-module-checker :allowed-modules %s]"
required-module
current-module
current-module)
(not allowed-module-namespace?)
(format "Namespace %s is not an allowed external API namespace for the %s module. [:metabase/ns-module-checker :api-namespaces %s]"
required-namespace
required-module
required-module))]
(hooks/reg-finding! (assoc (meta node)
:message error
:type :metabase/ns-module-checker)))))))))
(defn lint-ns [x]
(lint-require-shapes (:node x))
(lint-modules (:node x) (get-in x [:config :linters :metabase/ns-module-checker]))
x)

View File

@@ -1,160 +0,0 @@
(ns hooks.clojure.test
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]
[hooks.common]))
(defn- warn-about-disallowed-parallel-forms [form config]
(letfn [(error! [form message]
(hooks/reg-finding! (assoc (meta form)
:message message
:type :metabase/validate-deftest)))
(f [form]
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
(cond
(contains? (:parallel/unsafe config) qualified-symbol)
(error! form (format "%s is not allowed inside a ^:parallel test or test fixture [:metabase/validate-deftest]" qualified-symbol))
(and (not (contains? (:parallel/safe config) qualified-symbol))
(str/ends-with? (name qualified-symbol) "!"))
(error! form (format "destructive functions like %s are not allowed inside a ^:parallel test or test fixture. If this should be allowed, add it to the whitelist in the Kondo config file [:metabase/validate-deftest]"
qualified-symbol)))))
(walk [form]
(f form)
(doseq [child (:children form)]
(walk child)))]
(walk form)))
(defn- deftest-check-parallel
"1. Check if test is marked ^:parallel / ^:synchronized correctly
2. Make sure disallowed forms are not used in ^:parallel tests"
[{[_ test-name & body] :children, :as _node} config]
(let [test-metadata (:meta test-name)
metadata-sexprs (map hooks/sexpr test-metadata)
combined-metadata (transduce
(map (fn [x]
(if (map? x)
x
{x true})))
(completing merge)
{}
metadata-sexprs)
parallel? (:parallel combined-metadata)
synchronized? (:synchronized combined-metadata)]
(when (and parallel? synchronized?)
(hooks/reg-finding! (assoc (meta test-name)
:message "Test should not be marked both ^:parallel and ^:synchronized"
:type :metabase/validate-deftest)))
;; only when the custom `:metabase/deftest-not-marked-parallel-or-synchronized` is enabled: complain if tests are
;; not explicitly marked `^:parallel` or `^:synchronized`. This is mostly to encourage people to mark everything
;; `^:parallel` in places like `metabase.lib` tests unless there is a really good reason not to.
(when-not (or parallel? synchronized?)
(hooks/reg-finding!
(assoc (meta test-name)
:message "Test should be marked either ^:parallel or ^:synchronized"
:type :metabase/deftest-not-marked-parallel-or-synchronized)))
(when parallel?
(doseq [form body]
(warn-about-disallowed-parallel-forms form config)))))
(defn- deftest-check-not-horrifically-long
[node {:keys [max-length], :as _config}]
(let [{:keys [row end-row]} (meta node)]
(when (and row end-row)
(let [num-lines (- end-row row)]
(when (and max-length
(>= num-lines max-length))
(hooks/reg-finding! (assoc (meta node)
:message (str (format "This test is horrifically long, it's %d lines! 😱 " num-lines)
"Do you really want to try to debug it if it fails? 💀 "
"Split it up into smaller tests! 🥰")
:type :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests)))))))
(defn- ignore? [node error-type]
(when-let [ignores (some-> node meta :clj-kondo/ignore hooks/sexpr)]
(when-let [ignores (cond
(coll? ignores) (set ignores)
(keyword? ignores) #{ignores})]
(contains? ignores error-type))))
(defn deftest-check-no-driver-keywords [node {:keys [drivers], :as _config}]
;; fail fast after we see the first error, where we see one hardcoded driver name there are likely several more and we
;; don't need multiple warnings for a single test.
(letfn [(f [node]
(when (and (hooks/keyword-node? node)
(set? drivers)
(contains? drivers (hooks/sexpr node)))
(hooks/reg-finding! (assoc (meta node)
:message (format "Do not hardcode driver name %s in driver tests! [:metabase/disallow-hardcoded-driver-names-in-tests]"
(hooks/sexpr node))
:type :metabase/disallow-hardcoded-driver-names-in-tests))
::error))
(walk [node]
(cond
(ignore? node :metabase/disallow-hardcoded-driver-names-in-tests)
nil
(= (f node) ::error)
::error
:else
(reduce
(fn [_acc child]
(when (= (walk child) ::error)
(reduced ::error)))
nil
(:children node))))]
(walk node)))
(defn deftest [{:keys [node cljc lang config]}]
;; run [[deftest-check-parallel]] only once... if this is a `.cljc` file only run it for the `:clj` analysis, no point
;; in running it twice.
(when (or (not cljc)
(= lang :clj))
(deftest-check-parallel node (get-in config [:linters :metabase/validate-deftest])))
(deftest-check-not-horrifically-long node (get-in config [:linters :metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests]))
(deftest-check-no-driver-keywords node (get-in config [:linters :metabase/disallow-hardcoded-driver-names-in-tests]))
{:node node})
;;; this is a hacky way to determine whether these namespaces are required in the `ns` form or not... basically `:ns`
;;; will come back as `nil` if they are not.
(defn- approximately-equal-ns-required? []
(= (:ns (hooks/resolve {:name 'metabase.test-runner.assert-exprs.approximately-equal/=?-report}))
'metabase.test-runner.assert-exprs.approximately-equal))
(defn- malli-equals-ns-required? []
(= (:ns (hooks/resolve {:name 'metabase.test-runner.assert-exprs.malli-equals/malli=-report}))
'metabase.test-runner.assert-exprs.malli-equals))
(defn- warn-about-missing-test-expr-requires-in-cljs [{:keys [children], :as _is-node}]
(let [[_is assertion-node] children]
(when (hooks/list-node? assertion-node)
(let [[assertion-symb-node] (:children assertion-node)]
(when (hooks/token-node? assertion-symb-node)
(let [assertion-symb-token (hooks/sexpr assertion-symb-node)
warn! (fn [ns-to-require]
(hooks/reg-finding!
(assoc (meta assertion-symb-node)
:message (format "You must require %s to use %s in ClojureScript"
ns-to-require
assertion-symb-token)
:type :metabase/missing-test-expr-requires-in-cljs)))]
(condp = assertion-symb-token
'=?
(when-not (approximately-equal-ns-required?)
(warn! 'metabase.test-runner.assert-exprs.approximately-equal))
'malli=
(when-not (malli-equals-ns-required?)
(warn! 'metabase.test-runner.assert-exprs.malli-equals))
nil)))))))
(defn is [{:keys [node lang]}]
(when (= lang :cljs)
(warn-about-missing-test-expr-requires-in-cljs node))
{:node node})
(defn use-fixtures [{:keys [node config]}]
(warn-about-disallowed-parallel-forms node config)
{:node node})

View File

@@ -1,423 +0,0 @@
(ns hooks.common
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.pprint]))
(defn with-macro-meta
"When introducing internal nodes (let, defn, etc) it is important to provide a meta of an existing token
as the current version of kondo will use the whole form by default."
[new-node hook-node]
(with-meta new-node (meta (first (:children hook-node)))))
;;; This stuff is to help debug hooks. Trace a function and it will pretty-print the before & after sexprs.
;;;
;;; (hooks.common/trace #'calculate-bird-scarcity)
(defn- trace* [f]
{:pre [(fn? f)]}
(fn traced-fn [node]
(println \newline)
(clojure.pprint/pprint (hooks/sexpr (:node node)))
(let [node* (f node)]
(println '=>)
(clojure.pprint/pprint (hooks/sexpr (:node node*)))
node*)))
;;; this doesn't seem to work with SCI. Use [[defn-traced]] instead.
;; (defn trace [varr]
;; {:pre [(var? varr)]}
;; (when-not (::traced? (meta varr))
;; (alter-var-root varr trace*)
;; (alter-meta! varr assoc ::traced? true)
;; (println "Traced" varr)))
(defmacro defn-traced
"Replace a hook [[defn]] with this to trace the before and after sexprs."
[fn-name & fn-tail]
`(do
(defn fn# ~@fn-tail)
(def ~fn-name (trace* fn#))))
;;;; Common hook definitions
(defn do*
"This is the same idea as [[clojure.core/do]] but doesn't cause Kondo to complain about redundant dos or unused values."
[{{[_ & args] :children, :as node} :node}]
(let [node* (-> (hooks/list-node
(list*
(with-meta (hooks/token-node 'do) {:clj-kondo/ignore [:redundant-do]})
(for [arg args]
(vary-meta arg update :clj-kondo/ignore #(conj (vec %) :unused-value)))))
(with-meta (meta node)))]
{:node node*}))
(defn with-one-binding
"Helper for macros that have a shape like
(my-macro [x]
...)
=>
(let [x nil]
...)
Binding is optional and `_` will be substituted if not supplied."
[{{[_ {[x] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-two-bindings
"Helper for macros that have a shape like
(my-macro [x y]
...)
=>
(let [x nil, y nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[x y] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
(or y (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-three-bindings
"Helper for macros that have a shape like
(my-macro [x y z]
...)
=>
(let [x nil, y nil, z nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[x y z] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or x (hooks/token-node '_)) (hooks/token-node 'nil)
(or y (hooks/token-node '_)) (hooks/token-node 'nil)
(or z (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-four-bindings
"Helper for macros that have a shape like
(my-macro [a b c d]
...)
=>
(let [a nil, b nil, c nil, d nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-five-bindings
"Helper for macros that have a shape like
(my-macro [a b c d e]
...)
=>
(let [a nil, b nil, c nil, d nil, e nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d e] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
(or e (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-six-bindings
"Helper for macros that have a shape like
(my-macro [a b c d e f]
...)
=>
(let [a nil, b nil, c nil, d nil, e nil, f nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d e f] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
(or e (hooks/token-node '_)) (hooks/token-node 'nil)
(or f (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-seven-bindings
"Helper for macros that have a shape like
(my-macro [a b c d e f g]
...)
=>
(let [a nil, b nil, c nil, d nil, e nil, f nil, g nil]
...)
All bindings are optional and `_` will be substituted if not supplied."
[{{[_ {[a b c d e f g] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[(or a (hooks/token-node '_)) (hooks/token-node 'nil)
(or b (hooks/token-node '_)) (hooks/token-node 'nil)
(or c (hooks/token-node '_)) (hooks/token-node 'nil)
(or d (hooks/token-node '_)) (hooks/token-node 'nil)
(or e (hooks/token-node '_)) (hooks/token-node 'nil)
(or f (hooks/token-node '_)) (hooks/token-node 'nil)
(or g (hooks/token-node '_)) (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-one-top-level-binding
"Helper for macros that have a shape like
(my-macro x
...)
=>
(let [x nil]
...)"
[{{[_ x & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[x (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn with-two-top-level-bindings
"Helper for macros that have a shape like
(my-macro x y
...)
=>
(let [x nil, y nil]
...)"
[{{[_ x y & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[x (hooks/token-node 'nil)
y (hooks/token-node 'nil)])
body))]
{:node node*}))
(defn let-one-with-optional-value
"This is exactly like [[clojure.core/let]] but the right-hand side of the binding, `value`, is optional, and only one
binding can be established.
(some-macro [x] ...)
=>
(let [x nil] ...)
(some-macro [x 100] ...)
=>
(let [x 100] ...)"
[{{[_ {[binding value] :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[binding (or value (hooks/token-node 'nil))])
body))]
{:node node*}))
(defn- let-second-inner [body bindings]
(let [binding-infos (for [[model {[binding value] :children}] (partition 2 bindings)]
{:model model
:binding binding
:value (or value
(hooks/token-node 'nil))})]
(-> (hooks/vector-node
[(hooks/vector-node (map :model binding-infos))
(-> (hooks/list-node (list* (hooks/token-node `let)
(hooks/vector-node (mapcat (juxt :binding :value) binding-infos))
body))
(with-meta (meta body)))])
(with-meta (meta body)))))
(defn let-second
"Helper for macros that have a shape like
(my-macro x [y]
...)
where the let is for second arg.
=>
(let [y nil]
...)"
[{:keys [node]}]
(let [[_ first-arg-ref binding+opts & body] (:children node)]
{:node (let-second-inner body [first-arg-ref binding+opts])}))
(defn let-with-optional-value-for-last-binding
"This is exactly like [[clojure.core/let]] but the right-hand side of the *last* binding, `value`, is optional.
(some-macro [x] ...)
=>
(let [x nil] ...)
(some-macro [x 100] ...)
=>
(let [x 100] ...)"
[{{[_ {bindings :children} & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
(into []
(comp (partition-all 2)
(mapcat (fn [[binding value]]
[binding (or value (hooks/token-node 'nil))])))
bindings))
body))]
{:node node*}))
(defn with-ignored-first-arg
"For macros like
(discard-setting-changes [setting-1 setting-2]
...)
=>
(do ...)
where the first arg ought to be ignored for linting purposes."
[{{[_ _x & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'do)
body))]
{:node node*}))
(defn with-used-first-arg
"For macros like
(with-drivers (filter pred? some-drivers)
...)
=>
(let [_1234 (filter pred? some-drivers)]
...)
where the first arg should be linted and appear to be used."
[{{[_ arg & body] :children} :node}]
(let [node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node [(hooks/token-node (gensym "_"))
arg])
body))]
{:node node*}))
(defn node->qualified-symbol [node]
(try
(when (hooks/token-node? node)
(let [sexpr (hooks/sexpr node)]
(when (symbol? sexpr)
(let [resolved (hooks/resolve {:name sexpr})]
(cond
(and resolved (:ns resolved))
(symbol (name (:ns resolved)) (name (:name resolved)))
;; if it wasn't resolved but is still qualified it's probably using the full namespace name rather than an
;; alias.
(qualified-symbol? sexpr)
sexpr)))))
;; some symbols like `*count/Integer` aren't resolvable.
(catch Exception _
nil)))
(defn format-string-specifier-count
"Number of things like `%s` in a format string, not counting newlines (`%n`) or escaped percent signs (`%%`). For
checking the number of args to something that takes a format string."
[format-string]
(count (re-seq #"(?<!%)%(?![%n])" format-string)))
(comment
;; should be 1
(format-string-specifier-count "%s %%")
;; should be 2
(format-string-specifier-count "%s %%%n%s")
;; should be 0
(format-string-specifier-count "%n%%%%")
;; should be 1
(format-string-specifier-count "%-02d"))
(defn add-lsp-ignore-unused-public-var-metadata
"Add
^{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
metadata to a node to suppress LSP warnings."
[node]
(letfn [(update-ignores [existing-ignores]
(hooks/vector-node
(cons
(hooks/keyword-node :clojure-lsp/unused-public-var)
(:children existing-ignores))))]
(vary-meta node update :clj-kondo/ignore update-ignores)))

View File

@@ -1,32 +0,0 @@
(ns hooks.metabase.api.common
(:require
[clj-kondo.hooks-api :as api]
[clojure.string :as str]
[hooks.common]))
(defn route-fn-name
"route fn hook"
[method route]
(let [route (if (vector? route) (first route) route)]
(-> (str (name method) route)
(str/replace #"/" "_")
symbol)))
(defn defendpoint
[arg]
(letfn [(update-defendpoint [node]
(let [[_defendpoint method route & body] (:children node)]
(-> (api/list-node
(list
(api/token-node 'do)
(-> (api/token-node (symbol "compojure.core" (str (api/sexpr method))))
(with-meta (meta method)))
(api/list-node
(list*
(api/token-node 'clojure.core/defn)
(-> (api/token-node (route-fn-name (api/sexpr method) (api/sexpr route)))
(with-meta (meta route)))
body))))
(with-meta (meta node))
hooks.common/add-lsp-ignore-unused-public-var-metadata)))]
(update arg :node update-defendpoint)))

View File

@@ -1,18 +0,0 @@
(ns hooks.metabase.driver
(:require
[clj-kondo.hooks-api :as api]))
(defn register!
"Register the driver keyword so LSP can jump to it."
[x]
(letfn [(update-driver-keyword [k]
(api/reg-keyword! k 'metabase.driver/register!))
(update-children [[_register! k :as children]]
(if (api/keyword-node? k)
(update (vec children) 1 update-driver-keyword)
children))
(update-node [node]
(if (api/list-node? node)
(update node :children update-children)
node))]
(update x :node update-node)))

View File

@@ -1,17 +0,0 @@
(ns hooks.metabase.util
(:require
[clj-kondo.hooks-api :as api]
[hooks.common]))
(defn format-color [{:keys [node], :as x}]
(let [[_format-color _color format-string-node & args] (:children node)]
(when (api/string-node? format-string-node)
(let [expected-arg-count (hooks.common/format-string-specifier-count (api/sexpr format-string-node))
actual-arg-count (count args)]
(when-not (= expected-arg-count actual-arg-count)
(api/reg-finding! (assoc (meta node)
:message (format "metabase.util/format-color format string expects %d arguments instead of %d."
expected-arg-count
actual-arg-count)
:type :format))))))
x)

View File

@@ -1,126 +0,0 @@
(ns hooks.metabase.util.log
(:require
[clj-kondo.hooks-api :as api]
[clojure.string :as str]
[clojure.walk :as walk]
[hooks.common]))
;;;; Checks for both log + logf
(defn- disallow-i18n [node]
(walk/postwalk
(fn [x]
(when-let [qualified-symbol (hooks.common/node->qualified-symbol x)]
(when (and (= (namespace qualified-symbol) "metabase.util.i18n")
(or (str/includes? (name qualified-symbol) "tru")
(str/includes? (name qualified-symbol) "trs")))
(api/reg-finding! (assoc (meta x)
:message "do not i18n the logs!"
:type :metabase/validate-logging)))))
node))
(defn- check-position-of-exception-arg
"Tell people to do (info e \"whatever\") instead of (info \"whatever\" e)."
[node]
(let [[f _first-arg & args] (:children node)]
(doseq [arg args]
(when (and (api/token-node? arg)
(= (api/sexpr arg) 'e))
(api/reg-finding! (assoc (meta arg)
:message (format "pass exceptions as the first arg to %s" (api/sexpr f))
:type :metabase/validate-logging))))))
(defn- common-checks [node]
(check-position-of-exception-arg node)
(disallow-i18n node))
(defn- check-for-i18n-format-specifiers
"Make sure people aren't using things like {0} in their log messages."
[node string-node]
(when (re-find #"\{\d+\}" (api/sexpr string-node))
(api/reg-finding! (assoc (meta node)
:message "this looks like an i18n format string. Don't use identifiers like {0} in logging."
:type :metabase/validate-logging))))
;;; log only
(defn- check-not-format-string
"Make sure we're not using something like log/trace with a format string."
[{[f] :children, :as node} string-node]
(when (pos? (hooks.common/format-string-specifier-count (api/sexpr string-node)))
(api/reg-finding! (assoc (meta node)
:message (format "%s used with a format string, use %s instead."
(api/sexpr f)
(str (api/sexpr f) "f"))
:type :metabase/validate-logging))))
(defn- format-node? [node]
(when (api/list-node? node)
(let [[x] (:children node)]
(and (api/token-node? x)
(or (= (hooks.common/node->qualified-symbol x) 'clojure.core/format)
(= (api/sexpr x) 'format))))))
(defn- warn-against-log+format-instead-of-logf
"Tell people to use things like infof instead of info + format."
[{[f] :children, :as node} [x :as args]]
(when (and (= (count args) 1)
(api/list-node? x))
(when (format-node? x)
(api/reg-finding! (assoc (meta node)
:message (format "Use %s instead of %s + format"
(str (api/sexpr f) "f")
(api/sexpr f))
:type :metabase/validate-logging)))))
(defn info
"Valid log, debug, etc."
[{:keys [node], :as x}]
(common-checks node)
(let [[_f & args] (:children node)
[_e & [x :as args]] (if (and (api/token-node? (first args))
(or (api/string-node? (second args))
(format-node? (second args))))
args
(cons nil args))]
(when (api/string-node? x)
(check-for-i18n-format-specifiers node x)
(check-not-format-string node x))
(warn-against-log+format-instead-of-logf node args))
x)
;;;; logf only
(defn- check-format-string-arg-count
"Make sure the number of args passed to something like tracef match the number of format string specifiers."
[{[f] :children, :as node} format-string args]
(let [expected-arg-count (hooks.common/format-string-specifier-count (api/sexpr format-string))
actual-arg-count (count args)]
(when (zero? expected-arg-count)
(api/reg-finding! (assoc (meta node)
:message (format "Don't use %s with no format string arguments, use %s instead."
(api/sexpr f)
(str/replace (str (api/sexpr f)) #"f$" ""))
:type :metabase/validate-logging)))
(when-not (= expected-arg-count actual-arg-count)
(api/reg-finding! (assoc (meta node)
:message (format "log format string expects %d arguments instead of %d."
expected-arg-count
actual-arg-count)
:type :metabase/validate-logging)))))
(defn infof
"Valid logf, debugf, etc."
[{:keys [node], :as x}]
(common-checks node)
(let [[_f & args] (:children node)
[_e format-string & args] (if (api/string-node? (first args))
(cons nil args)
args)]
;; TODO -- maybe we can error if you did not pass a format string, e.g. (log/infof 1 2) is obviously incorrect.
(when (api/string-node? format-string)
(check-for-i18n-format-specifiers node format-string)
(check-format-string-arg-count node format-string args)))
x)

View File

@@ -1,26 +0,0 @@
(ns hooks.metabase.util.log.capture
(:require
[clj-kondo.hooks-api :as api]))
(defn with-log-messages-for-level [x]
(letfn [(update-binding [[lhs-node _rhs-node]]
[lhs-node (api/list-node
(list
(api/token-node 'clojure.core/fn)
(api/vector-node [])))])
(update-bindings [bindings-node]
(-> (api/vector-node
(into []
(comp (partition-all 2)
(mapcat update-binding))
(:children bindings-node)))
(with-meta (meta bindings-node))))
(update-node [node]
(let [[_with-log-messages-for-level bindings & body] (:children node)]
(-> (api/list-node
(list*
(api/token-node 'clojure.core/let)
(update-bindings bindings)
body))
(with-meta (meta node)))))]
(update x :node update-node)))

View File

@@ -1,52 +0,0 @@
(ns hooks.clojure.test-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.test :refer :all]
[hooks.clojure.test]))
(set! *warn-on-reflection* true)
(defn- deftest-warnings
[form]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/disallow-hardcoded-driver-names-in-tests {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.clojure.test/deftest {:node (api/parse-string form)
:config {:linters
{:metabase/disallow-hardcoded-driver-names-in-tests
{:drivers
#{:athena}}}}})
(mapv :message @(:findings clj-kondo.impl.utils/*ctx*))))
(deftest ^:parallel disallow-hardcoded-driver-names-in-tests-test
(is (= []
(deftest-warnings
"(mt/test-drivers (mt/normal-drivers)
(do-something))")))
(is (= ["Do not hardcode driver name :athena in driver tests! [:metabase/disallow-hardcoded-driver-names-in-tests]"]
(deftest-warnings
"(mt/test-drivers (mt/normal-drivers)
(when-not (= driver/*driver* :athena)
(do-something)))")))
(testing "make sure :clj-kondo/ignore is propagated correctly"
(is (= []
(deftest-warnings
"(mt/test-drivers (mt/normal-drivers)
#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
(when-not (= driver/*driver* :athena)
(do-something)))")))))
(deftest ^:parallel check-driver-keywords-test
(testing "Make sure we keep hooks.clojure.test/driver-keywords up to date"
(let [driver-keywords (-> (slurp ".clj-kondo/config.edn")
edn/read-string
(get-in [:linters :metabase/disallow-hardcoded-driver-names-in-tests :drivers]))]
(doseq [^java.io.File file (.listFiles (io/file "modules/drivers"))
:when (.isDirectory file)
:let [driver (keyword (.getName file))]]
(is (contains? driver-keywords driver)
(format "hooks.clojure.test/driver-keywords should contain %s, please add it" driver))))))

View File

@@ -1,19 +0,0 @@
(ns ^:mb/once hooks.common-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.test :refer :all]
[hooks.common]))
(deftest ^:parallel node->qualified-symbol-test
(binding [clj-kondo.impl.utils/*ctx* {:namespaces (atom nil)}]
(is (= 'metabase.util.i18n/tru
(hooks.common/node->qualified-symbol (api/parse-string "metabase.util.i18n/tru"))))))
(deftest ^:parallel node->qualified-symbol-test-2
(binding [clj-kondo.impl.utils/*ctx* {:namespaces (atom {:clj {:clj '{hooks.common-test {:qualify-ns {i18n metabase.util.i18n}}}}})
:lang :clj
:base-lang :clj
:ns {:name 'hooks.common-test}}]
(is (= 'metabase.util.i18n/tru
(hooks.common/node->qualified-symbol (api/parse-string "i18n/tru"))))))

View File

@@ -1,35 +0,0 @@
(ns hooks.metabase.api.common-test
(:require
[clj-kondo.hooks-api :as api]
[clojure.test :refer :all]
[hooks.metabase.api.common]))
(deftest ^:parallel defendpoint-test
(is (= '(do
compojure.core/POST
(clojure.core/defn
POST_:id_copy
"Copy a `Card`, with the new name 'Copy of _name_'"
[id]
{id [:maybe ms/PositiveInt]}
(let [orig-card (api/read-check Card id)
new-name (str (trs "Copy of ") (:name orig-card))
new-card (assoc orig-card :name new-name)]
(-> (card/create-card! new-card @api/*current-user*)
hydrate-card-details
(assoc :last-edit-info (last-edit/edit-information-for-user @api/*current-user*))))))
(-> {:node (-> '(api/defendpoint POST "/:id/copy"
"Copy a `Card`, with the new name 'Copy of _name_'"
[id]
{id [:maybe ms/PositiveInt]}
(let [orig-card (api/read-check Card id)
new-name (str (trs "Copy of ") (:name orig-card))
new-card (assoc orig-card :name new-name)]
(-> (card/create-card! new-card @api/*current-user*)
hydrate-card-details
(assoc :last-edit-info (last-edit/edit-information-for-user @api/*current-user*)))))
pr-str
api/parse-string)}
hooks.metabase.api.common/defendpoint
:node
api/sexpr))))

View File

@@ -1,19 +0,0 @@
(ns hooks.metabase.util.log.capture-test
(:require
[clj-kondo.hooks-api :as api]
[clojure.test :refer :all]
[hooks.metabase.util.log.capture]))
(deftest ^:parallel with-log-messages-for-level-test
(is (= '(clojure.core/let [messages-1 (clojure.core/fn [])
messages-2 (clojure.core/fn [])]
(concat (messages-1)
(messages-2)))
(-> (hooks.metabase.util.log.capture/with-log-messages-for-level
{:node (-> '(with-log-messages-for-level [messages-1 [my.namespace :debug]
messages-2 :info]
(concat (messages-1) (messages-2)))
pr-str
api/parse-string)})
:node
api/sexpr))))

View File

@@ -1,77 +0,0 @@
(ns ^:mb/once hooks.metabase.util.log-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.string :as str]
[clojure.test :refer :all]
[hooks.metabase.util.log]))
(defn- warnings
[form]
(let [f (if (str/ends-with? (name (first form)) "f")
hooks.metabase.util.log/infof
hooks.metabase.util.log/info)]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-logging {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(f {:node (api/parse-string (pr-str form))})
(mapv :message @(:findings clj-kondo.impl.utils/*ctx*)))))
(deftest ^:parallel warn-on-missing-format-args-test
(testing "should fail, missing a format arg"
(are [form] (= ["log format string expects 1 arguments instead of 0."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock was acquired after %d retries.")
(metabase.util.log/warnf e "Migration lock was acquired after %d retries."))))
(deftest ^:parallel warn-on-too-many-format-args-test-1
(testing "should fail, too many format args"
(are [form] (= ["log format string expects 1 arguments instead of 2."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock was acquired after %d retries." 1 2)
(metabase.util.log/warnf e "Migration lock was acquired after %d retries." 1 2))))
(deftest ^:parallel warn-on-too-many-format-args-test-2
(testing "should fail, too many format args"
(are [form] (= ["this looks like an i18n format string. Don't use identifiers like {0} in logging."
"Don't use metabase.util.log/warnf with no format string arguments, use metabase.util.log/warn instead."
"log format string expects 0 arguments instead of 1."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock was acquired after {0} retries." 1)
(metabase.util.log/warnf e "Migration lock was acquired after {0} retries." 1))))
(deftest ^:parallel warn-on-format-args-without-logf-test
(testing "should fail, has format args but is warn rather than warnf"
(are [form] (= ["metabase.util.log/warn used with a format string, use metabase.util.log/warnf instead."]
(warnings (quote form)))
(metabase.util.log/warn "Migration lock was acquired after %d retries." 1)
(metabase.util.log/warn e "Migration lock was acquired after %d retries." 1))))
(deftest ^:parallel warn-on-format-without-logf-test
(testing "should fail, has format args but is warn rather than warnf"
(are [form] (= ["Use metabase.util.log/warnf instead of metabase.util.log/warn + format"]
(warnings (quote form)))
(metabase.util.log/warn (format "Migration lock was acquired after %d retries." 1))
(metabase.util.log/warn e (format "Migration lock was acquired after %d retries." 1)))))
(deftest ^:parallel warn-on-logf-with-no-format-args-test
(testing "should fail, has format args but is warn rather than warnf"
(are [form] (= ["Don't use metabase.util.log/warnf with no format string arguments, use metabase.util.log/warn instead."]
(warnings (quote form)))
(metabase.util.log/warnf "Migration lock cleared.")
(metabase.util.log/warnf e "Migration lock cleared."))))
(deftest ^:parallel warn-on-i18n-test-1
(testing "should fail -- should not use i18n/tru or i18n/trs"
(are [form] (= ["do not i18n the logs!"]
(warnings (quote form)))
(metabase.util.log/warnf (metabase.util.i18n/tru "Migration lock was acquired after {0} retries." 1))
(metabase.util.log/warnf e (metabase.util.i18n/tru "Migration lock was acquired after {0} retries." 1)))))
(deftest ^:parallel warn-on-i18n-test-2
(testing "should fail -- should not use i18n/tru or i18n/trs"
(are [form] (= ["do not i18n the logs!"]
(warnings (quote form)))
(metabase.util.log/warn (metabase.util.i18n/trs "Migration lock was acquired after {0} retries." 1))
(metabase.util.log/warn e (metabase.util.i18n/trs "Migration lock was acquired after {0} retries." 1)))))

View File

@@ -1,339 +1,31 @@
;; Cljfmt config. See https://github.com/weavejester/cljfmt#formatting-options
{:sort-ns-references?
true
:function-arguments-indentation
:community
:parallel?
true
:paths
["src"
"test"
"enterprise/backend"
"bin/build"
"modules/drivers"
#_"dev"]
;; See https://github.com/weavejester/cljfmt/blob/master/cljfmt/resources/cljfmt/indents/clojure.clj for the default
;; definitions
;;
;; [[:block 0]]
;; ^ number of special args
;;
;; `:block` indentation means format with all arguments indented 2 spaces except for the 'special' arguments which
;; normally are supposed to go on the first line. e.g. `let` gets [[:block 1]] because it has one 'special' arg.
;;
;; [[:inner 0]]
;; ^ depth
;; [[:inner 2 0]]
;; ^ argument index to restrict this rule to
;;
;; `:inner` indentation is like `:defn` indentation. `[[:inner 0]]` (default for `defn`) means all arguments at depth
;; zero get indented 2 spaces if not on the first line regardless of number of elements on the first line.
:extra-indents
{;; clojure.core stuff
fn* [[:inner 0]]
let* [[:block 1]]
with-meta [[:default]]
;; library stuff. Libraries that specify their own `:style/indent` specs are included in the section after this
;; since it is generated automatically.
clojure.core.logic/fresh [[:block 1]]
clojure.core.logic/matcha [[:block 1]]
clojure.core.logic/run [[:block 2]]
clojure.core.match/match [[:block 1]]
clojure.test.check.properties/for-all [[:block 1]]
clojure.test.check.properties/for-all* [[:block 1]]
clojure.test.check/quick-check [[:block 1]]
clojure.tools.macro/macrolet [[:block 1] [:inner 2 0]]
clojurewerkz.quartzite.jobs/build [[:default]]
clojurewerkz.quartzite.jobs/with-description [[:default]]
clojurewerkz.quartzite.jobs/with-identity [[:default]]
clojurewerkz.quartzite.schedule.cron/schedule [[:default]]
clojurewerkz.quartzite.schedule.cron/with-misfire-handling-instruction-do-nothing [[:default]]
clojurewerkz.quartzite.triggers/build [[:default]]
clojurewerkz.quartzite.triggers/with-description [[:default]]
clojurewerkz.quartzite.triggers/with-identity [[:default]]
clojurewerkz.quartzite.triggers/with-priority [[:default]]
clojurewerkz.quartzite.triggers/with-schedule [[:default]]
def-map-type [[:block 2] [:inner 1]]
defprotocol+ [[:block 1] [:inner 1]]
defrecord+ [[:block 2] [:inner 1]]
deftype+ [[:block 2] [:inner 1]]
net.cgrand.macrovich/case [[:block 0]]
potemkin.namespaces/import-vars [[:default]]
potemkin.types/def-map-type [[:block 2] [:inner 1]]
potemkin.types/defprotocol+ [[:block 1] [:inner 1]]
potemkin.types/defrecord+ [[:block 2] [:inner 1]]
potemkin.types/deftype+ [[:block 2] [:inner 1]]
potemkin/def-map-type [[:block 2] [:inner 1]]
potemkin/defprotocol+ [[:block 1] [:inner 1]]
potemkin/defrecord+ [[:block 2] [:inner 1]]
potemkin/deftype+ [[:block 2] [:inner 1]]
potemkin/import-vars [[:default]]
taoensso.nippy/extend-freeze [[:inner 0]]
taoensso.nippy/extend-thaw [[:inner 0]]
throttle.core/with-throttling [[:block 1]]
;; the rest of the stuff in this list is generated automatically based on `:style/indent` specs
;; by running [[dev.generate-cljfmt-config/specs]]... use that code to update this list
metabase-enterprise.advanced-permissions.api.util-test/with-impersonations! [[:inner 0]]
metabase-enterprise.cache.config-test/with-temp-persist-models [[:block 1]]
metabase-enterprise.sandbox.test-util/with-gtaps! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.sandbox.test-util/with-user-attributes [[:block 2]]
metabase-enterprise.serialization.test-util/with-dbs [[:inner 0]]
metabase-enterprise.serialization.test-util/with-random-dump-dir [[:block 1]]
metabase-enterprise.serialization.test-util/with-world [[:block 0]]
metabase-enterprise.sso.integrations.jwt-test/with-users-with-email-deleted [[:block 1]]
metabase-enterprise.test/with-gtaps! [[:inner 0]]
metabase-enterprise.test/with-gtaps-for-user! [[:inner 0]]
metabase-enterprise.test/with-user-attributes [[:block 2]]
metabase.actions-test/with-actions-test-data-and-actions-permissively-enabled [[:block 0]]
metabase.actions.test-util/with-actions [[:block 1]]
metabase.actions.test-util/with-actions-disabled [[:block 0]]
metabase.actions.test-util/with-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data [[:inner 0]]
metabase.actions.test-util/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.actions.test-util/with-actions-test-data-tables [[:block 1]]
metabase.actions.test-util/with-empty-db [[:inner 0]]
metabase.actions.test-util/with-temp-test-data [[:inner 0]]
metabase.api.alert-test/with-alert-in-collection [[:block 1]]
metabase.api.card-test/with-cards-in-readable-collection [[:block 1]]
metabase.api.card-test/with-cards-in-writeable-collection [[:block 1]]
metabase.api.card-test/with-ordered-items [[:inner 0]]
metabase.api.card-test/with-temp-native-card! [[:block 1]]
metabase.api.card-test/with-temp-native-card-with-params! [[:block 1]]
metabase.api.collection-test/with-collection-hierarchy [[:block 1]]
metabase.api.collection-test/with-french-user-and-personal-collection [[:block 2]]
metabase.api.collection-test/with-some-children-of-collection [[:block 1]]
metabase.api.common/check [[:default]]
metabase.api.common/define-routes [[:block 0]]
metabase.api.common/let-404 [[:block 1]]
metabase.api.common.internal/auto-coerce [[:block 1]]
metabase.api.common.internal/auto-parse [[:block 1]]
metabase.api.common.internal-test/no-route-regexes [[:block 0]]
metabase.api.embed-test/do-response-formats [[:block 1]]
metabase.api.embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.api.embed-test/with-embedding-enabled-and-temp-card-referencing [[:block 3]]
metabase.api.embed-test/with-embedding-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.api.embed-test/with-new-secret-key [[:block 0]]
metabase.api.embed-test/with-temp-card [[:block 1]]
metabase.api.embed-test/with-temp-dashcard [[:block 1]]
metabase.api.preview-embed-test/with-embedding-enabled-and-new-secret-key [[:block 0]]
metabase.api.preview-embed-test/with-new-secret-key [[:block 0]]
metabase.api.public-test/with-sharing-enabled-and-temp-card-referencing [[:block 3]]
metabase.api.public-test/with-sharing-enabled-and-temp-dashcard-referencing [[:block 3]]
metabase.api.public-test/with-temp-public-card [[:block 1]]
metabase.api.public-test/with-temp-public-dashboard [[:block 1]]
metabase.api.public-test/with-temp-public-dashboard-and-card [[:block 1]]
metabase.api.search-test/do-test-users [[:block 1]]
metabase.api.session/http-401-on-error [[:block 0]]
metabase.async.streaming-response/streaming-response [[:block 2]]
metabase.async.streaming-response-test/with-streaming-response-thread-pool [[:block 0]]
metabase.async.streaming-response-test/with-test-driver-db! [[:block 0]]
metabase.async.util/cancelable-thread [[:block 0]]
metabase.cmd.copy/step [[:block 1]]
metabase.cmd.copy/with-connection-rollback-only [[:block 1]]
metabase.cmd.copy/with-disabled-db-constraints [[:block 2]]
metabase.config/build-type-case [[:block 0]]
metabase.dashboard-subscription-test/with-link-card-fixture-for-dashboard [[:block 2]]
metabase.db.liquibase/with-liquibase [[:block 1]]
metabase.db.liquibase/with-scope-locked [[:block 1]]
metabase.db.schema-migrations-test.impl/test-migrations [[:block 2]]
metabase.db/with-application-db [[:inner 0]]
metabase.driver.bigquery-cloud-sdk.query-processor/with-temporal-type [[:default]]
metabase.driver.druid.client/DELETE [[:default]]
metabase.driver.druid.client/GET [[:default]]
metabase.driver.druid.client/POST [[:default]]
metabase.driver.druid.query-processor-test/druid-query [[:block 0]]
metabase.driver.druid.query-processor-test/druid-query-returning-rows [[:block 0]]
metabase.driver.mongo.query-processor/mongo-let [[:block 1]]
metabase.driver.sql-jdbc.actions/with-jdbc-transaction [[:block 1]]
metabase.driver.sql-jdbc.connection/with-connection-spec-for-testing-connection [[:block 1]]
metabase.driver.sql-jdbc.execute.diagnostic/capturing-diagnostic-info [[:block 1]]
metabase.driver.sql.query-processor/apply-top-level-clause [[:default]]
metabase.driver/with-driver [[:block 1]]
metabase.lib.binning/with-binning [[:default]]
metabase.lib.core/with-binning [[:default]]
metabase.lib.core/with-join-alias [[:default]]
metabase.lib.core/with-join-conditions [[:default]]
metabase.lib.join/with-join-alias [[:default]]
metabase.lib.join/with-join-conditions [[:default]]
metabase.lib.test-util.macros/$ids [[:inner 0]]
metabase.lib.test-util.macros/mbql-query [[:inner 0]]
metabase.lib.util.match/match [[:inner 0]]
metabase.lib.util.match/match-one [[:inner 0]]
metabase.lib.util.match/replace [[:inner 0]]
metabase.lib.util.match/replace-in [[:inner 0]]
metabase.models.collection-test/with-collection-hierarchy [[:block 1]]
metabase.models.collection-test/with-current-user-perms-for-collections [[:block 1]]
metabase.models.collection-test/with-personal-and-impersonal-collections [[:block 1]]
metabase.models.dashboard-test/with-dash-in-collection [[:inner 0]]
metabase.models.interface/define-batched-hydration-method [[:inner 0]]
metabase.models.interface/define-simple-hydration-method [[:inner 0]]
metabase.models.params.chain-filter-test/with-clean-field-values-for-field [[:block 1]]
metabase.models.params.chain-filter-test/with-fk-field-to-field-remapping [[:block 0]]
metabase.models.params.chain-filter-test/with-human-readable-values-remapping [[:block 0]]
metabase.models.pulse-test/with-dashboard-subscription-in-collection [[:block 1]]
metabase.models.pulse-test/with-pulse-in-collection [[:inner 0]]
metabase.models.setting/defsetting [[:block 1]]
metabase.models.setting.multi-setting/define-multi-setting [[:inner 0]]
metabase.models.task-history/with-task-history [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/pmbql-query [[:block 1]]
metabase.query-processor-test.pipeline-queries-test/run-pmbql-query [[:block 1]]
metabase.query-processor.debug/debug> [[:block 0]]
metabase.query-processor.error-type/deferror [[:block 1]]
metabase.query-processor.middleware.cache-backend.interface/with-cached-results [[:block 4]]
metabase.query-processor.middleware.process-userland-query-test/with-query-execution [[:block 1]]
metabase.query-processor.store/cached [[:block 1]]
metabase.query-processor.store/with-metadata-provider [[:inner 0]]
metabase.query-processor.streaming/streaming-response [[:block 1]]
metabase.query-processor.test-util/format-rows-by [[:default]]
metabase.sample-data-test/with-temp-sample-database-db [[:block 1]]
metabase.server.middleware.session/as-admin [[:block 0]]
metabase.server.middleware.session/with-current-user [[:inner 0]]
metabase.shared.util.i18n/trs [[:default]]
metabase.shared.util.i18n/trsn [[:default]]
metabase.shared.util.i18n/tru [[:default]]
metabase.shared.util.i18n/trun [[:default]]
metabase.shared.util.namespaces/import-fns [[:default]]
metabase.sync.fetch-metadata/log-if-error [[:block 1]]
metabase.sync.sync-metadata.fields-test/with-test-db [[:block 0]]
metabase.sync.util/do-with-start-and-finish-debug-logging [[:default]]
metabase.sync.util/sum-for [[:block 1]]
metabase.sync.util/sync-operation [[:block 3]]
metabase.sync.util/with-db-logging-disabled [[:default]]
metabase.sync.util/with-duplicate-ops-prevented [[:default]]
metabase.sync.util/with-emoji-progress-bar [[:block 1]]
metabase.sync.util/with-error-handling [[:block 1]]
metabase.sync.util/with-start-and-finish-logging [[:default]]
metabase.sync.util/with-start-and-finish-logging* [[:default]]
metabase.sync.util/with-sync-events [[:default]]
metabase.test/$ids [[:inner 0]]
metabase.test/as-admin [[:inner 0]]
metabase.test/dataset [[:inner 0]]
metabase.test/discard-setting-changes [[:block 1]]
metabase.test/format-rows-by [[:default]]
metabase.test/let-url [[:block 1]]
metabase.test/mbql-query [[:inner 0]]
metabase.test/native-query [[:block 0]]
metabase.test/query [[:block 1]]
metabase.test/run-mbql-query [[:inner 0]]
metabase.test/test-driver [[:inner 0]]
metabase.test/test-drivers [[:inner 0]]
metabase.test/test-helpers-set-global-values! [[:block 0]]
metabase.test/throw-if-called! [[:block 1]]
metabase.test/with-actions [[:block 1]]
metabase.test/with-actions-disabled [[:block 0]]
metabase.test/with-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data [[:inner 0]]
metabase.test/with-actions-test-data-and-actions-enabled [[:block 0]]
metabase.test/with-actions-test-data-tables [[:block 1]]
metabase.test/with-additional-premium-features [[:block 1]]
metabase.test/with-current-user [[:inner 0]]
metabase.test/with-driver [[:block 1]]
metabase.test/with-empty-db [[:inner 0]]
metabase.test/with-empty-h2-app-db [[:block 0]]
metabase.test/with-metadata-provider [[:inner 0]]
metabase.test/with-premium-features [[:block 1]]
metabase.test/with-temp [[:inner 0]]
metabase.test/with-temp-copy-of-db [[:block 0]]
metabase.test/with-temp-scheduler [[:block 0]]
metabase.test/with-temp-test-data [[:inner 0]]
metabase.test/with-temp-vals-in-db [[:block 3]]
metabase.test/with-test-user [[:inner 0]]
metabase.test/with-user-in-groups [[:inner 0]]
metabase.test.data/$ids [[:inner 0]]
metabase.test.data/dataset [[:inner 0]]
metabase.test.data/mbql-query [[:inner 0]]
metabase.test.data/native-query [[:block 0]]
metabase.test.data/query [[:block 1]]
metabase.test.data/run-mbql-query [[:inner 0]]
metabase.test.data/with-empty-h2-app-db [[:block 0]]
metabase.test.data/with-temp-copy-of-db [[:block 0]]
metabase.test.data.datasets/test-driver [[:inner 0]]
metabase.test.data.datasets/test-drivers [[:inner 0]]
metabase.test.data.one-off-dbs/with-blank-db [[:block 0]]
metabase.test.data.one-off-dbs/with-blueberries-db [[:block 0]]
metabase.test.data.users/with-test-user [[:inner 0]]
metabase.test.util/discard-setting-changes [[:block 1]]
metabase.test.util/let-url [[:block 1]]
metabase.test.util/throw-if-called! [[:block 1]]
metabase.test.util/with-temp-scheduler [[:block 0]]
metabase.test.util/with-temp-vals-in-db [[:block 3]]
metabase.test.util/with-user-in-groups [[:inner 0]]
metabase.test.util.public-settings/with-additional-premium-features [[:block 1]]
metabase.test.util.public-settings/with-premium-features [[:block 1]]
metabase.test.util.thread-local/test-helpers-set-global-values! [[:block 0]]
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
metabase.upload-test/with-upload-table! [[:inner 0]]
metabase.upload.types/with-currency [[:default]]
metabase.upload.types/with-parens [[:default]]
metabase.util/auto-retry [[:block 1]]
metabase.util/ignore-exceptions [[:block 0]]
metabase.util/or-with [[:block 1]]
metabase.util/profile [[:block 1]]
metabase.util/prog1 [[:inner 0]]
metabase.util/varargs [[:inner 0]]
metabase.util/with-us-locale [[:block 0]]
metabase.util.date-2/with-time-zone-same-instant [[:default]]
metabase.util.encryption-test/with-secret-key [[:block 1]]
metabase.util.honey-sql-2/with-database-type-info [[:default]]
metabase.util.i18n/deferred-trs [[:default]]
metabase.util.i18n/deferred-tru [[:default]]
metabase.util.i18n/trs [[:default]]
metabase.util.i18n/tru [[:default]]
metabase.util.jvm/auto-retry [[:block 1]]
metabase.util.jvm/varargs [[:block 1]]
metabase.util.jvm/with-us-locale [[:block 0]]
metabase.util.malli/disable-enforcement [[:block 0]]
metabase.util.malli/with [[:default]]
metabase.util.malli/with-api-error-message [[:default]]
metabuild-common.core/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.core/step [[:block 1]]
metabuild-common.entrypoint/exit-when-finished-nonzero-on-exception [[:block 0]]
metabuild-common.steps/step [[:block 1]]
methodical.core/defmethod [[:inner 0]]
methodical.core/defmulti [[:inner 0]]
methodical.core/with-dispatcher [[:default]]
methodical.core/with-method-table [[:default]]
methodical.core/with-prefers [[:default]]
methodical.interface/with-dispatcher [[:default]]
methodical.interface/with-method-table [[:default]]
methodical.interface/with-prefers [[:default]]
methodical.macros/defmethod [[:inner 0]]
methodical.macros/defmulti [[:inner 0]]
saml20-clj.sp.response/validate-confirmation-datas [[:block 1]]
toucan2.connection/with-transaction [[:block 1]]
toucan2.core/build [[:block 0]]
toucan2.core/compile [[:block 0]]
toucan2.core/debug [[:block 0]]
toucan2.core/define-after-insert [[:inner 0]]
toucan2.core/define-after-select [[:inner 0]]
toucan2.core/define-after-update [[:inner 0]]
toucan2.core/define-before-delete [[:inner 0]]
toucan2.core/define-before-insert [[:inner 0]]
toucan2.core/define-before-select [[:inner 0]]
toucan2.core/define-default-fields [[:inner 0]]
toucan2.core/define-named-query [[:block 1]]
toucan2.core/deftransforms [[:block 1]]
toucan2.core/with-transaction [[:block 1]]
toucan2.tools.after/define-after-impl [[:default]]
toucan2.tools.after-insert/define-after-insert [[:inner 0]]
toucan2.tools.after-select/define-after-select [[:inner 0]]
toucan2.tools.after-update/define-after-update [[:inner 0]]
toucan2.tools.before-delete/define-before-delete [[:inner 0]]
toucan2.tools.before-insert/define-before-insert [[:inner 0]]
toucan2.tools.before-select/define-before-select [[:inner 0]]
toucan2.tools.compile/build [[:block 0]]
toucan2.tools.compile/compile [[:block 0]]
toucan2.tools.compile/resolved [[:block 0]]
toucan2.tools.debug/debug [[:block 0]]
toucan2.tools.default-fields/define-default-fields [[:inner 0]]
toucan2.tools.named-query/define-named-query [[:block 1]]
toucan2.tools.simple-out-transform/define-out-transform [[:inner 0]]
toucan2.tools.transformed/deftransforms [[:block 1]]
toucan2.tools.with-temp/with-temp [[:inner 0]]
toucan2.util/try-with-error-context [[:inner 0]]}}
;; https://github.com/weavejester/cljfmt/blob/master/cljfmt/resources/cljfmt/indents/clojure.clj
{:extra-indents
{auto-parse [[:inner 0]]
cond-> [[:inner 0]]
check [[:inner 0]]
deferred-tru [[:inner 0]]
defprotocol+ [[:block 1] [:inner 1]]
potemkin/defprotocol+ [[:block 1] [:inner 1]]
let-404 [[:block 1]]
metabase.legacy-mbql.util/match-one [[:inner 0]]
merge-with [[:inner 0]]
metabase.test/dataset [[:inner 0]]
metabase.test/formatted-rows [[:inner 0]]
metabase.test/mbql-query [[:inner 0]]
metabase.test/test-drivers [[:inner 0]]
metabase.test/with-temporary-setting-values [[:block 1]]
;; If we just treat these as functions we can ignore this, despite some files not being consistent
;metabase.models.permissions/set-has-full-permissions-for-set? [[:inner 0]]
;metabase.models.permissions/set-has-full-permissions? [[:inner 0]]
;metabase.models.permissions/set-has-partial-permissions? [[:inner 0]]
metabase.models.permissions/set-has-partial-permissions-for-set? [[:inner 0]]
publish-event! [[:block 1]]
metabase.query-processor.streaming/streaming-response [[:inner 0]]
metabase.util/prog1 [[:inner 0]]
metabase.util/select-keys-when [[:inner 0]]
;; This supports a lot of existing formatting, but these are functions so the
;; existing formatting is probably wrong.
;toucan2.core/select [[:inner 0]]
;toucan2.core/update! [[:inner 0]]
}
:sort-ns-references? true}

View File

@@ -20,72 +20,70 @@
(clojure-mode
;; Specify which arg is the docstring for certain macros
;; (Add more as needed)
(eval . (put 'defendpoint-schema 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint-async 'clojure-doc-string-elt 3))
(eval . (put 'define-premium-feature 'clojure-doc-string-elt 2))
(eval . (put 'api/defendpoint-schema 'clojure-doc-string-elt 3))
(eval . (put 'api/defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'api/defendpoint-async 'clojure-doc-string-elt 3))
(eval . (put 'defsetting 'clojure-doc-string-elt 2))
(eval . (put 'setting/defsetting 'clojure-doc-string-elt 2))
(eval . (put 's/defn 'clojure-doc-string-elt 2))
(eval . (put 'p.types/defprotocol+ 'clojure-doc-string-elt 2))
(eval . (put 'methodical/defmethod 'clojure-doc-string-elt 3))
(eval . (put 'methodical/defmulti 'clojure-doc-string-elt 2))
(eval . (put 'mi/define-simple-hydration-method 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint-schema 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'defendpoint-async 'clojure-doc-string-elt 3))
(eval . (put 'define-premium-feature 'clojure-doc-string-elt 2))
(eval . (put 'api/defendpoint-schema 'clojure-doc-string-elt 3))
(eval . (put 'api/defendpoint 'clojure-doc-string-elt 3))
(eval . (put 'api/defendpoint-async 'clojure-doc-string-elt 3))
(eval . (put 'defsetting 'clojure-doc-string-elt 2))
(eval . (put 'setting/defsetting 'clojure-doc-string-elt 2))
(eval . (put 's/defn 'clojure-doc-string-elt 2))
(eval . (put 'p.types/defprotocol+ 'clojure-doc-string-elt 2))
(eval . (put 'methodical/defmethod 'clojure-doc-string-elt 3))
(eval . (put 'methodical/defmulti 'clojure-doc-string-elt 2))
(eval . (put 'mi/define-simple-hydration-method 'clojure-doc-string-elt 3))
(eval . (put 'mi/define-batched-hydration-method 'clojure-doc-string-elt 3))
(eval . (put 'mr/def 'clojure-doc-string-elt 2))
(eval . (put 'mu/defn 'clojure-doc-string-elt 2))
(eval . (put 'mu/defn 'clojure-doc-string-elt 2))
;; Define custom indentation for functions inside metabase.
;; This list isn't complete; add more forms as we come across them.
;;
;; `put-clojure-indent' is a safe-local-eval-function, so use a bunch of calls to that
;; instead of one call to `define-clojure-indent'
(eval . (put-clojure-indent 'api/let-404 1))
(eval . (put-clojure-indent 'c/step 1))
(eval . (put-clojure-indent 'impl/test-migrations 2))
(eval . (put-clojure-indent 'let-404 0))
(eval . (put-clojure-indent 'lib.schema.match/match '(:defn)))
(eval . (put-clojure-indent 'lib.schema.match/match-one '(:defn)))
(eval . (put-clojure-indent 'lib.schema.match/replace '(:defn)))
(eval . (put-clojure-indent 'lib.schema.match/replace-in '(:defn)))
(eval . (put-clojure-indent 'lib.util.match/replace '(:defn)))
(eval . (put-clojure-indent 'macros/case 0))
(eval . (put-clojure-indent 'match 1))
(eval . (put-clojure-indent 'mt/dataset 1))
(eval . (put-clojure-indent 'mt/format-rows-by '(:form)))
(eval . (put-clojure-indent 'mt/query 1))
(eval . (put-clojure-indent 'mt/test-driver 1))
(eval . (put-clojure-indent 'mt/test-drivers 1))
(eval . (put-clojure-indent 'mt/with-anaphora 1))
(eval . (put-clojure-indent 'prop/for-all 1))
(eval . (put-clojure-indent 'api/let-404 1))
(eval . (put-clojure-indent 'c/step 1))
(eval . (put-clojure-indent 'impl/test-migrations 2))
(eval . (put-clojure-indent 'let-404 0))
(eval . (put-clojure-indent 'lib.schema.match/match 1))
(eval . (put-clojure-indent 'lib.schema.match/match-one 1))
(eval . (put-clojure-indent 'lib.schema.match/replace 1))
(eval . (put-clojure-indent 'lib.schema.match/replace-in 2))
(eval . (put-clojure-indent 'lib.util.match/replace 1))
(eval . (put-clojure-indent 'macros/case 0))
(eval . (put-clojure-indent 'match 1))
(eval . (put-clojure-indent 'mi/define-methods '(:form)))
(eval . (put-clojure-indent 'mt/dataset 1))
(eval . (put-clojure-indent 'mt/format-rows-by 1))
(eval . (put-clojure-indent 'mt/query 1))
(eval . (put-clojure-indent 'mt/test-driver 1))
(eval . (put-clojure-indent 'mt/test-drivers 1))
(eval . (put-clojure-indent 'prop/for-all 1))
(eval . (put-clojure-indent 'qp.streaming/streaming-response 1))
(eval . (put-clojure-indent 'tc/quick-check 1))
(eval . (put-clojure-indent 'u/profile 1))
(eval . (put-clojure-indent 'u/prog1 1))
(eval . (put-clojure-indent 'u/select-keys-when '(:form)))
(eval . (put-clojure-indent 'with-meta '(:form)))
(eval . (put-clojure-indent 'tc/quick-check 1))
(eval . (put-clojure-indent 'u/profile 1))
(eval . (put-clojure-indent 'u/prog1 1))
(eval . (put-clojure-indent 'u/select-keys-when 1))
(eval . (put-clojure-indent 'with-meta '(:form)))
;; these ones have to be done with `define-clojure-indent' for now because of upstream bug
;; https://github.com/clojure-emacs/clojure-mode/issues/600 once that's resolved we should use `put-clojure-indent'
;; instead. Please don't add new entries unless they don't work with `put-clojure-indent'
(eval . (define-clojure-indent
(l/matcha '(1 (:defn)))
(l/matche '(1 (:defn)))
(l/matcha '(1 (:defn)))
(l/matche '(1 (:defn)))
(p.types/def-abstract-type '(1 (:defn)))
(p.types/defprotocol+ '(1 (:defn)))
(p.types/defrecord+ '(2 nil nil (:defn)))
(p.types/deftype+ '(2 nil nil (:defn)))
(p/def-map-type '(2 nil nil (:defn)))
(p/defprotocol+ '(1 (:defn)))
(p/defrecord+ '(2 nil nil (:defn)))
(p/deftype+ '(2 nil nil (:defn)))
(tools.macro/macrolet '(1 ((:defn)) :form))))
(p.types/defprotocol+ '(1 (:defn)))
(p.types/defrecord+ '(2 nil nil (:defn)))
(p.types/deftype+ '(2 nil nil (:defn)))
(p/def-map-type '(2 nil nil (:defn)))
(p/defprotocol+ '(1 (:defn)))
(p/defrecord+ '(2 nil nil (:defn)))
(p/deftype+ '(2 nil nil (:defn)))
(tools.macro/macrolet '(1 ((:defn)) :form))))
(cider-clojure-cli-aliases . "dev:drivers:drivers-dev:ee:ee-dev:user")
(clojure-indent-style . always-align)
(cljr-favor-prefix-notation . nil)
(cljr-insert-newline-after-require . t)
(cljr-print-right-margin . 118)
(clojure-docstring-fill-column . 118)
(cider-preferred-build-tool . clojure-cli)
(cider-default-cljs-repl . shadow-select)

210
.eslintrc Normal file
View File

@@ -0,0 +1,210 @@
{
"rules": {
"strict": [2, "never"],
"no-undef": 2,
"no-var": 1,
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"no-empty": [1, { "allowEmptyCatch": true }],
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "moment",
"message": "Moment is deprecated, please use dayjs"
},
{
"name": "moment-timezone",
"message": "Moment is deprecated, please use dayjs"
}
]
}
],
"curly": [1, "all"],
"eqeqeq": [1, "smart"],
"import/no-default-export": 2,
"import/no-named-as-default": 0,
"import/no-commonjs": 1,
"import/order": [
"error",
{
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"orderImportKind": "asc",
"caseInsensitive": false
},
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"warnOnUnassignedImports": false
}
],
"no-console": [2, { "allow": ["warn", "error", "errorBuffer"] }],
"react/no-is-mounted": 2,
"react/prefer-es6-class": 2,
"react/display-name": 1,
"react/prop-types": 2,
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 0,
"react/no-find-dom-node": 0,
"react/no-children-prop": 2,
"react/no-string-refs": 2,
"react/no-unescaped-entities": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-key": 2,
"react/forbid-component-props": [2, { "forbid": ["sx"] }],
"react-hooks/exhaustive-deps": [
"warn",
{ "additionalHooks": "(useSyncedQueryString|useSafeAsyncFunction)" }
],
"prefer-const": [1, { "destructuring": "all" }],
"no-useless-escape": 0,
"no-only-tests/no-only-tests": [
"error",
{
"block": [
"describe",
"it",
"context",
"test",
"tape",
"fixture",
"serial",
"Feature",
"Scenario",
"Given",
"And",
"When",
"Then",
"describeWithSnowplow",
"describeEE"
]
}
],
"complexity": ["error", { "max": 54 }]
},
"globals": {
"before": true,
"cy": true,
"Cypress": true
},
"env": {
"browser": true,
"es6": true,
"commonjs": true,
"jest": true,
"jest/globals": true
},
"parser": "babel-eslint",
"plugins": ["react", "no-only-tests"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
],
"settings": {
"import/internal-regex": "^metabase/|^metabase-lib/",
"import/resolver": {
"webpack": {
"typescript": true
}
},
"import/ignore": ["\\.css$"],
"react": {
"version": "detect"
}
},
"parserOptions": {
"ecmaFeatures": {
"legacyDecorators": true
}
},
"overrides": [
{
"files": ["*.js", "*.jsx", "*.ts", "*.tsx"],
"rules": {
"no-unconditional-metabase-links-render": "error",
"no-literal-metabase-strings": "error"
}
},
{
"files": [
"*.unit.spec.*",
"frontend/src/metabase/admin/**/*",
"frontend/src/metabase/setup/**/*",
"frontend/lint/**/*",
"*.stories.*",
"e2e/**/*",
"**/tests/*",
"release/**/*"
],
"rules": {
"no-unconditional-metabase-links-render": "off",
"no-literal-metabase-strings": "off"
}
},
{
"extends": ["plugin:@typescript-eslint/recommended"],
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"prefer-rest-params": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true,
"destructuredArrayIgnorePattern": "^_"
}
],
// This was introduced in 6.0.0
"@typescript-eslint/no-unsafe-declaration-merging": "off"
}
},
{
"extends": [
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react"
],
"plugins": ["jest", "jest-dom", "testing-library"],
"files": [
"*.unit.spec.ts",
"*.unit.spec.tsx",
"*.unit.spec.js",
"*.unit.spec.jsx"
],
"rules": {
"jest/valid-title": ["error", { "ignoreTypeOfDescribeName": true }],
// In unit tests, you should use `act(() => {})` to invoke things causing state updates
// and rerenders to remove lengthy warnings from jest. Disabling the rule allows this.
"testing-library/no-unnecessary-act": "off"
}
}
]
}

View File

@@ -1,250 +0,0 @@
/* eslint-disable import/no-commonjs */
/* eslint-disable no-undef */
// `postcss-modules` lints css modules class names, but it currently crashes
// eslint on vscode. If you use webstorm or want to run the lint for the cli, you
// can use this flag to enable it. This is set to true in CI
const shouldLintCssModules =
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
module.exports = {
rules: {
strict: [2, "never"],
"no-undef": 2,
"no-var": 1,
"no-unused-vars": [
"error",
{
vars: "all",
args: "none",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"no-empty": [1, { allowEmptyCatch: true }],
"no-restricted-imports": [
"error",
{
paths: [
{
name: "moment",
message: "Moment is deprecated, please use dayjs",
},
{
name: "moment-timezone",
message: "Moment is deprecated, please use dayjs",
},
],
},
],
curly: [1, "all"],
eqeqeq: [1, "smart"],
"import/no-default-export": 2,
"import/no-named-as-default": 0,
"import/no-commonjs": 1,
"import/order": [
"error",
{
"newlines-between": "always",
alphabetize: {
order: "asc",
orderImportKind: "asc",
caseInsensitive: false,
},
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
warnOnUnassignedImports: false,
},
],
"sort-imports": [
"error",
{
// allows this rule to work with import/order
ignoreDeclarationSort: true,
},
],
"no-console": [2, { allow: ["warn", "error", "errorBuffer"] }],
"react/no-is-mounted": 2,
"react/prefer-es6-class": 2,
"react/display-name": 1,
"react/prop-types": 2,
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 0,
"react/no-find-dom-node": 0,
"react/no-children-prop": 2,
"react/no-string-refs": 2,
"react/no-unescaped-entities": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-key": 2,
"react/forbid-component-props": [2, { forbid: ["sx"] }],
"react-hooks/exhaustive-deps": [
"warn",
{ additionalHooks: "(useSafeAsyncFunction)" },
],
"prefer-const": [1, { destructuring: "all" }],
"no-restricted-globals": ["error", "close"],
"no-useless-escape": 0,
"no-only-tests/no-only-tests": [
"error",
{
block: [
"describe",
"it",
"context",
"test",
"tape",
"fixture",
"serial",
"Feature",
"Scenario",
"Given",
"And",
"When",
"Then",
"describeWithSnowplow",
"describeEE",
],
},
],
complexity: ["error", { max: 54 }],
...(shouldLintCssModules
? {
"postcss-modules/no-undef-class": "error",
}
: {}),
},
globals: {
before: true,
cy: true,
Cypress: true,
},
env: {
browser: true,
es6: true,
commonjs: true,
jest: true,
"jest/globals": true,
},
parser: "babel-eslint",
plugins: ["react", "no-only-tests", "postcss-modules"],
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:depend/recommended",
],
settings: {
"import/internal-regex": "^metabase/|^metabase-lib/",
"import/resolver": {
webpack: {
typescript: true,
},
},
"import/ignore": ["\\.css$"],
react: {
version: "detect",
},
"postcss-modules": {
baseDir: "./frontend/src",
},
},
parserOptions: {
ecmaFeatures: {
legacyDecorators: true,
},
},
overrides: [
{
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
rules: {
"no-unconditional-metabase-links-render": "error",
"no-literal-metabase-strings": "error",
"depend/ban-dependencies": [
"error",
{
allowed: [
"underscore",
"moment",
"lodash.orderby",
"lodash.debounce",
],
},
],
},
},
{
files: [
"*.unit.spec.*",
"frontend/src/metabase/admin/**/*",
"frontend/src/metabase/setup/**/*",
"frontend/lint/**/*",
"*.stories.*",
"e2e/**/*",
"**/tests/*",
"release/**/*",
],
rules: {
"no-unconditional-metabase-links-render": "off",
"no-literal-metabase-strings": "off",
},
},
{
extends: ["plugin:@typescript-eslint/recommended"],
files: ["*.ts", "*.tsx"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
rules: {
"prefer-rest-params": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "^_",
},
],
// This was introduced in 6.0.0
"@typescript-eslint/no-unsafe-declaration-merging": "off",
},
},
{
extends: [
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react",
],
plugins: ["jest", "jest-dom", "testing-library"],
files: [
"*.unit.spec.ts",
"*.unit.spec.tsx",
"*.unit.spec.js",
"*.unit.spec.jsx",
],
rules: {
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
},
},
],
};

13
.github/CODEOWNERS vendored
View File

@@ -1,16 +1,3 @@
# Before changing this file, make sure you have read how we use it:
# https://www.notion.so/metabase/Pull-Request-Code-Review-Guide-374f05df889748b69d67af753f9bc9b8?pvs=4#36550c04ceab40b5a8b8bee6264b090c
# TL;DR: if @camsaul hasn't been involved you're probably doing it wrong
src/metabase @camsaul
modules @camsaul
enterprise/backend @camsaul
enterprise/backend/src/metabase_enterprise/advanced_permissions @noahmoss
enterprise/backend/src/metabase_enterprise/audit_app @noahmoss
enterprise/backend/src/metabase_enterprise/sso @noahmoss
enterprise/backend/src/metabase_enterprise/task/truncate_audit_table.clj @noahmoss
enterprise/backend/src/metabase_enterprise/internal_user.clj @noahmoss
frontend/src/metabase/ui @metabase/core-frontend-admin-webapp
src/metabase/**/*permissions* @noahmoss
src/metabase/integrations @noahmoss
snowplow/* @metabase/data

View File

@@ -17,6 +17,7 @@ A clear and concise description of what you want to happen.
A clear and concise description of any alternative solutions or features you've considered.
**How important is this feature to you?**
Note: the more honest and specific you are here the more we will take you seriously.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,5 +1,5 @@
> [!IMPORTANT]
> For those employed by Metabase: if you are merging into master, please add either a `backport` or a `no-backport` label to this PR. You will not be able to merge until you do this step. Refer to the section [Do I need to backport this PR?](https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#89f80d6f17714a0198aeb66c0efd1b71) in the Metabase Branching Strategy document for more details. If you're not employed by Metabase, this section does not apply to you, and the label will be taken care of by your reviewer.
> If you are merging into master, please add either a `backport` or a `no-backport` label to this PR. You will not be able to merge until you do this step. Refer to the section [Do I need to backport this PR?](https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#89f80d6f17714a0198aeb66c0efd1b71) in the Metabase Branching Strategy document for more details.
> **Warning**
>

View File

@@ -16,6 +16,7 @@ runs:
const java = 11;
const defaultRunner = "ubuntu-22.04";
const beefierRunner = "buildjet-2vcpu-ubuntu-2204";
const defaultOptions = {
"java-version": java,
@@ -35,11 +36,8 @@ runs:
["dashboard-cards", {} ],
["dashboard-filters", {} ],
["embedding", {} ],
["embedding-sdk", { context: "embedding-sdk" } ],
["filters", {} ],
["filters-reproductions", {}],
["joins", {} ],
["metrics", {} ],
["models", {} ],
["native", {} ],
["native-filters", {} ],
@@ -47,14 +45,12 @@ runs:
["organization", {} ],
["permissions", {} ],
["question", {} ],
["question-reproductions", {} ],
["search", {}],
["sharing", {} ],
["visualizations-charts", {} ],
["visualizations-tabular", {} ],
["oss-subset", { edition: 'oss', context: "special" } ],
["slow", { context: "special" } ],
["flaky", { context: "special" } ],
["slow", { runner: beefierRunner, context: "special" } ],
["flaky", { runner: beefierRunner, context: "special" } ],
["mongo", { context: "qa-database" } ],
];

View File

@@ -1,67 +0,0 @@
name: Cross-branch migration test
inputs:
base-ref:
required: true
runs:
using: "composite"
steps:
- name: Check out base branch
uses: actions/checkout@v4
with:
ref: ${{ inputs.base-ref }}
- name: Prepare backend environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: cross-branch-migrations
- name: Run backend on base branch
run: |
clojure -M:run:ee > metabase.log 2>&1 & echo $! > metabase_pid
tail -F metabase.log &
shell: bash
- name: Wait for Metabase to start
run: |
while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}' > /dev/null; do
if ! kill -0 $(cat metabase_pid) 2>/dev/null; then
cat metabase.log
echo "Metabase process has exited."
exit 1
fi
sleep 1
done
shell: bash
- name: Stop Metabase on base branch
run: kill $(cat metabase_pid)
shell: bash
- name: Check out pull request branch
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Run backend on pull request branch
run: |
clojure -M:run:ee > metabase.log 2>&1 & echo $! > metabase_pid
tail -F metabase.log &
shell: bash
- name: Wait for Metabase to start
run: |
while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}' > /dev/null; do
if ! kill -0 $(cat metabase_pid) 2>/dev/null; then
cat metabase.log
echo "Metabase process has exited. Check that your migration changes work on an instance that has been previously initialized on your PR's target branch."
exit 1
fi
sleep 1
done
shell: bash
- name: Stop Metabase on pull request branch
run: kill $(cat metabase_pid)
shell: bash

View File

@@ -8,29 +8,26 @@ inputs:
description: DockerHub password/token
required: true
maildev:
description: Maildev container
description: Maildev
required: true
default: false
default: 'false'
openldap:
description: OpenLDAP container
description: Maildev
required: true
default: false
default: 'false'
postgres:
description: Postgres container
description: Maildev
required: true
default: false
default: 'false'
mysql:
description: MySQL container
description: Maildev
required: true
default: false
default: 'false'
mongo:
description: Mongo container
description: Maildev
required: true
default: false
webhook:
description: Webhook tester container
required: false
default: false
default: 'false'
runs:
using: "composite"
@@ -87,11 +84,4 @@ runs:
while ! nc -z localhost 27004; do sleep 1; done &&
echo -e "${G}Mongo is up and running!"
fi
if ${{ inputs.webhook }}; then
echo -e "${Y}Starting webhook test container..." &&
docker run -d -p 9080:8080/tcp tarampampam/webhook-tester serve --create-session 00000000-0000-0000-0000-000000000000 &&
while ! nc -z localhost 9080; do sleep 1; done &&
echo -e "${G}Webhook tester is up and running!"
fi
shell: bash

View File

@@ -1,20 +1,12 @@
name: Prepare cypress environment
description: Cypress preparation steps
outputs:
chrome-path:
description: Path to the custom Chrome executable
value: ${{ steps.setup-chrome.outputs.chrome-path }}
runs:
using: "composite"
steps:
- name: Install Chrome v111
uses: browser-actions/setup-chrome@v1
with:
# https://chromium.cypress.io/linux/stable/111.0.5563.146
chrome-version: 1097615
id: setup-chrome
- name: Install Replay.io browser
run: npx @replayio/cypress install
shell: bash
- name: Check to see if dependencies should be cached
if: ${{ contains(github.event.head_commit.message, '[ci nocache]') }}

View File

@@ -1,17 +1,11 @@
name: Prepare front-end environment
inputs:
node-version:
description: "The version of Node.js to use, overriding .nvmrc"
required: false
runs:
using: "composite"
steps:
- name: Prepare Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }} # this overrides, if present
node-version-file: ".nvmrc"
- name: Check to see if dependencies should be cached
if: ${{ contains(github.event.head_commit.message, '[ci nocache]') }}
run: |
@@ -24,27 +18,11 @@ runs:
with:
path: ~/.m2
key: ${{ runner.os }}-cljs-${{ hashFiles('**/shadow-cljs.edn') }}
- name: Get node_modules cache
uses: actions/cache@v4
id: node-modules-cache
if: ${{ !contains(github.event.head_commit.message, '[ci nocache]') }}
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock', '**/patches/*.patch') }}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
shell: bash
- uses: actions/cache@v4
if: steps.node-modules-cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile --prefer-offline
shell: bash

View File

@@ -1,33 +0,0 @@
name: Run Embedding SDK Storybook
runs:
using: "composite"
steps:
- name: Prepare frontend environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cljs"
- name: Compile CLJS
run: yarn build-pure:cljs
shell: bash
- name: Build Storybook
run: yarn build-storybook-embedding-sdk
shell: bash
- name: Serve Storybook
run: yarn http-server storybook-static -p 6006 &
shell: bash
- name: Wait for Storybook to become available
run: |
echo "Waiting for Storybook to be served..."
while ! curl --silent --fail http://localhost:6006; do
sleep 5
echo "Retrying..."
done
echo "Storybook is being served!"
shell: bash

View File

@@ -2,7 +2,7 @@ name: Test database driver
inputs:
junit-name:
required: true
default: "driver"
default: 'driver'
test-args:
required: false
@@ -14,7 +14,7 @@ runs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: driver
m2-cache-key: driver-${{ inputs.junit-name }}
- name: Build static viz frontend
run: yarn build-static-viz
shell: bash
@@ -25,7 +25,7 @@ runs:
uses: dorny/test-reporter@v1
if: failure()
with:
path: "target/junit/**/*_test.xml"
path: 'target/junit/**/*_test.xml'
name: JUnit Test Report ${{ inputs.junit-name }}
reporter: java-junit
list-suites: failed

View File

@@ -1,17 +0,0 @@
# The reviewers configuration for the auto_request_review workflow
# See https://github.com/necojackarc/auto-request-review/tree/master?tab=readme-ov-file#reviewers-configuration
# for the spec
files:
# migrations related files
'resources/migrations/**':
- team:core-backend-components
'src/metabase/db/custom_migrations.clj':
- team:core-backend-components
options:
ignore_draft: true
ignored_keywords:
- DO NOT REVIEW
- WIP
enable_group_assignment: false

View File

@@ -23,34 +23,24 @@ frontend_sources: &frontend_sources
- "**/tsconfig*.json"
- "package.json"
- "babel.config.json"
- ".eslintrc.js"
- ".eslintrc"
- "postcss.config.js"
- "webpack.config.js"
- "webpack.embedding-sdk.config.js"
- "webpack.static-viz.config.js"
- ".nvmrc"
frontend_specs: &frontend_specs
- *shared_specs
- "frontend/test/**"
- "frontend/**/tests/**"
- "frontend/**/*.unit.*"
- "jest.config.js"
- "jest.config.json"
- "jest.tz.unit.conf.json"
release_source: &release_sources
- "release/**"
frontend_all: &frontend_all
- *frontend_ci
- *frontend_sources
- *frontend_specs
frontend_loki_ci: &frontend_loki_ci
- ".github/workflows/loki.yml"
- ".loki/**"
- ".storybook/**"
backend_ci: &backend_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
@@ -61,7 +51,6 @@ backend_ci: &backend_ci
backend_presto_kerberos:
- "**/presto_jdbc/**"
- "**/presto_jdbc.clj"
- ".github/workflows/presto-kerberos-integration-test.yml"
backend_sources: &backend_sources
- *shared_sources
@@ -80,7 +69,6 @@ backend_specs: &backend_specs
- "modules/drivers/*/test/**"
- "test_config/**"
- "dev/**"
- "test_resources/**"
backend_all: &backend_all
- *default
@@ -110,10 +98,6 @@ e2e_specs: &e2e_specs
- "e2e/support/**"
- "e2e/snapshot*/**"
e2e_cross_version: &e2e_cross_version
- ".github/workflows/e2e-cross-version.yml"
- "e2e/test/scenarios/cross-version/**"
e2e_all:
- *default
- *e2e_ci

View File

@@ -1,68 +0,0 @@
exports.updateComment = updateComment;
/** @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments */
async function updateComment({ github, context }) {
const githubUsername = context.payload.sender.login;
const comment = `${CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER}
@${githubUsername} You have modified embedding SDK code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
Here are all the supported types that will be shown in the changelog:
- \`feat\`
- \`fix\`
- \`perf\`
These types \`docs\`, \`style\`, \`refactor\`, \`test\`, \`build\`, and \`ci\` are also encouraged for non-changelog related tasks.
Please also make sure to include \`sdk\` scope, otherwise, the changelog will not include this task.
For example, these are valid PR titles:
- \`feat(sdk): Add interactive dashboard component\`
- \`feat(sdk): Support theming pie chart\`
- \`fix(sdk): Fix static dashboard crash\``;
const owner = context.repo.owner;
const repo = context.repo.repo;
const pullRequestNumber = context.issue.number;
const comments = await github.rest.issues.listComments({
owner,
repo,
issue_number: pullRequestNumber,
});
const existingComment = getExistingConventionalCommitReminderComment(
comments.data,
);
if (existingComment) {
return await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body: comment,
});
} else {
return await github.rest.issues.createComment({
owner,
repo,
issue_number: pullRequestNumber,
body: comment,
});
}
}
const CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER =
"<!---conventional commit reminder comment ID-->";
/**
* @typedef Comment
* @property {string} body
*
* @param {Comment[]} comments
*/
function getExistingConventionalCommitReminderComment(comments) {
for (const comment of comments) {
if (
comment.body.startsWith(CONVENTIONAL_COMMIT_REMINDER_COMMENT_IDENTIFIER)
) {
return comment;
}
}
}

View File

@@ -1,110 +0,0 @@
const fs = require("fs");
const logger = { debug: console.debug, error: console.error };
const collectTestsByType = ({ type, suite, path, cache = [] }) => {
logger.debug(`Collecting tests of type ${type}`);
const localCache = cache;
const { [type]: typeList, suites, tests } = suite;
for (const uuid of typeList) {
const foundTestByUuid = tests.find(test => test.uuid === uuid);
if (!foundTestByUuid) {
logger.error(`Test with uuid ${uuid} not found`);
throw new Error(`Test with uuid ${uuid} not found`);
}
logger.debug(`Found test with uuid ${uuid}`);
foundTestByUuid.path = path;
localCache.push({ path, ...foundTestByUuid });
}
for (const subSuite of suites) {
collectTestsByType({
type,
suite: subSuite,
path: subSuite.file || path,
cache: localCache,
});
}
return localCache;
};
const extractTestResultsInfo = ({ results, stats }) => {
logger.debug("Extracting test results information");
const testType = "failures";
const failedTests = results.flatMap(result =>
collectTestsByType({
type: testType,
suite: result,
path: result.file,
}),
);
logger.debug("Finished extracting test results information");
return {
failedTests,
};
};
function parseReport() {
const content = fs.readFileSync(
"./cypress/reports/cypress-test-report.json",
"utf-8",
);
const data = JSON.parse(content);
return extractTestResultsInfo(data).failedTests.map(test => ({
title: test.fullTitle,
error: test.err?.message,
path: test.path,
}));
}
exports.parseReport = parseReport;
const groupTestsByPath = report => {
return report.reduce((acc, item) => {
if (!acc[item.path]) {
acc[item.path] = [];
}
acc[item.path].push(item);
return acc;
}, {});
};
function formatSummary(report) {
const failedTestsByPath = groupTestsByPath(report);
let summary = "### Failed Tests Summary\n\n";
for (const [path, tests] of Object.entries(failedTestsByPath)) {
summary += `| ${path} |\n`;
summary += "| :--- |\n";
tests.forEach(test => {
summary += `| ${test.title}`;
summary += "<p></p>"; // adds extra space after title
summary += `<details>`;
summary += "<summary>❌ ";
summary += `<code>${truncateError(test.error)}</code>`;
summary += "</summary>";
summary += `<pre>${test.error}</pre>`.replaceAll("\n", "<br>");
summary += `</details> |\n`;
});
}
return summary;
}
function truncateError(error, maxLength = 100) {
if (error.length <= maxLength) return error;
return error.replaceAll("\n", "").substring(0, maxLength) + "...";
}
exports.formatSummary = formatSummary;

41
.github/team.json vendored
View File

@@ -1,31 +1,21 @@
{
"teams": [
{
"name": "Querying",
"label": ".Team/Querying",
"projectUrl": "https://github.com/orgs/metabase/projects/97/views/1",
"name": "Querying Components",
"label": ".Team/QueryingComponents",
"members": [
"ranquild",
"uladzimirdev",
"kamilmielnik",
"oleggromov",
"nemanjaglumac",
"romeovs",
"bshepherdson",
"metamben",
"lbrdnk",
"appleby",
"uladzimirdev"
"romeovs"
]
},
{
"name": "Drivers",
"label": ".Team/Drivers",
"members": ["snoe"]
},
{
"name": "Embedding",
"label": ".Team/Embedding",
"projectUrl": "https://github.com/orgs/metabase/projects/88/views/1",
"members": ["WiNloSt", "deniskaber", "oisincoveney", "npretto", "heypoom"]
"members": ["WiNloSt", "deniskaber", "oisincoveney", "npretto"]
},
{
"name": "BEC",
@@ -34,21 +24,28 @@
"members": [
"tsmacdonald",
"qnkhuat",
"calherries",
"piranha",
"crisptrutski"
]
},
{
"name": "Query Processor",
"label": ".Team/QueryProcessor",
"projectUrl": "https://github.com/orgs/metabase/projects/85/views/1",
"members": ["camsaul", "snoe", "bshepherdson", "metamben", "lbrdnk"]
},
{
"name": "Admin Webapp",
"label": ".Team/AdminWebapp",
"members": [
"noahmoss",
"escherize",
"iethree",
"npfitz",
"johnswanson",
"rafpaf",
"sloansparger",
"iethree"
"sloansparger"
]
},
{
@@ -57,16 +54,10 @@
"members": [
"alxnddr",
"EmmadUsmani",
"JesseSDevaney",
"kulyk",
"adam-james-v"
]
},
{
"name": "DevEx",
"label": ".Team/DevEx",
"members": [
"camsaul"
]
}
]
}

View File

@@ -11,7 +11,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- uses: tibdex/github-app-token@v2.1.0
id: generate-token
with:
app_id: ${{ secrets.METABASE_BOT_APP_ID }}
private_key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- uses: ./.github/actions/find-squashed-commit
name: Find commit
id: find_commit
@@ -78,7 +82,7 @@ jobs:
TARGET_BRANCH: ${{ steps.get_latest_release_branch.outputs.branch-name }}
ORIGINAL_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
ORIGINAL_TITLE: ${{ github.event.pull_request.title }}
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
COMMIT: ${{ steps.find_commit.outputs.commit }}
- name: Auto approve backport PR
@@ -92,7 +96,7 @@ jobs:
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
uses: peter-evans/enable-pull-request-automerge@v2
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
token: ${{ steps.generate-token.outputs.token }}
pull-request-number: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
merge-method: squash

View File

@@ -1,21 +0,0 @@
# Config is in .github/auto_request_review.yml
name: Auto Request Review
on:
pull_request:
types: [opened, ready_for_review, reopened, synchronize]
jobs:
auto-request-review:
# Don't run on forked repos
if: ${{ !github.event.pull_request.head.repo.fork }}
permissions:
contents: read
pull-requests: write
name: Auto Request Review
runs-on: ubuntu-latest
steps:
- name: Request review based on files changes and/or groups the author belongs to
uses: necojackarc/auto-request-review@v0.13.0
with:
token: '${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}'

View File

@@ -1,27 +0,0 @@
name: Backend-Cloverage
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
jobs:
be-linter-cloverage:
runs-on: ubuntu-22.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cloverage"
- name: Build static viz frontend
run: yarn build-static-viz
- name: Collect the test coverage
run: clojure -X:dev:ci:ee:ee-dev:test:cloverage
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./target/coverage/codecov.json
flags: back-end

View File

@@ -12,6 +12,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
env:
CLJ_KONDO_VERSION: "2023.09.07"
jobs:
files-changed:
name: Check which files changed
@@ -60,6 +63,28 @@ jobs:
token: ${{ github.token }}
filters: .github/static-viz-sources.yaml
be-linter-cloverage:
needs: files-changed
if: (github.ref_name == 'master' && needs.files-changed.outputs.backend_all == 'true')
|| contains(github.event.pull_request.labels.*.name, 'run-cloverage')
runs-on: ubuntu-22.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cloverage"
- name: Build static viz frontend
run: yarn build-static-viz
- name: Collect the test coverage
run: clojure -X:dev:ci:ee:ee-dev:test:cloverage
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@v3
with:
files: ./target/coverage/codecov.json
flags: back-end
be-linter-clj-kondo:
needs: [files-changed, static-viz-files-changed]
if: |
@@ -67,12 +92,23 @@ jobs:
(needs.files-changed.outputs.backend_all == 'true' || needs.static-viz-files-changed.outputs.static_viz == 'true')
runs-on: ubuntu-22.04
timeout-minutes: 10
env:
DOWNLOAD_URL: https://github.com/clj-kondo/clj-kondo/releases/download
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: 'kondo'
- name: Install clj-kondo
run: |
curl -OL ${DOWNLOAD_URL}/v${CLJ_KONDO_VERSION}/clj-kondo-${CLJ_KONDO_VERSION}-linux-static-amd64.zip
curl -OL ${DOWNLOAD_URL}/v${CLJ_KONDO_VERSION}/clj-kondo-${CLJ_KONDO_VERSION}-linux-static-amd64.zip.sha256
cat clj-kondo-${CLJ_KONDO_VERSION}-linux-static-amd64.zip.sha256 >> SHA256sum.txt
echo " clj-kondo-${CLJ_KONDO_VERSION}-linux-static-amd64.zip" >> SHA256sum.txt
sha256sum -c SHA256sum.txt
unzip -d /usr/local/bin clj-kondo-${CLJ_KONDO_VERSION}-linux-static-amd64.zip
- run: clj-kondo --version
- name: Run clj-kondo
run: ./bin/kondo.sh
@@ -254,18 +290,3 @@ jobs:
steps:
- run: |
echo "Didn't run due to conditional filtering"
be-cljfmt:
needs: [files-changed]
if: |
!cancelled() && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: 'cljfmt'
- name: Run cljfmt
run: clojure -T:cljfmt check

View File

@@ -107,23 +107,8 @@ jobs:
echo "PR has been merged, searching for a squashed commit in the base branch"
echo "searching for a commit in a ${ORIGINAL_BASE_REF} that contains pull request number ${ORIGINAL_PULL_REQUEST_NUMBER}"
SQUASHED_COMMIT=$(env -i git log ${ORIGINAL_BASE_REF} --grep="(#${ORIGINAL_PULL_REQUEST_NUMBER})" --format="%H")
if [ -n "${SQUASHED_COMMIT}" ]; then
echo "found commit ${SQUASHED_COMMIT}"
git cherry-pick ${SQUASHED_COMMIT}
else
echo "probably a PR wasn't merged with Squash And Merge button, searching again"
SQUASHED_COMMIT=$(env -i git log ${ORIGINAL_BASE_REF} --grep="#${ORIGINAL_PULL_REQUEST_NUMBER}" --format="%H")
if [ -n "${SQUASHED_COMMIT}" ]; then
echo "found commit ${SQUASHED_COMMIT}"
git cherry-pick ${SQUASHED_COMMIT}
else
echo "No squashed commit found for PR #${ORIGINAL_PULL_REQUEST_NUMBER}"
exit 1
fi
fi
echo "found commit ${SQUASHED_COMMIT}"
git cherry-pick ${SQUASHED_COMMIT}
else
echo "PR has not been merged, copying all commits"
git cherry-pick ${ORIGINAL_BASE_SHA}..${ORIGINAL_HEAD_SHA}

View File

@@ -17,52 +17,30 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Send build start message
uses: actions/github-script@v7
uses: actions/github-script@v6
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');
const { sendReleaseMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
await sendPreReleaseMessage({
await sendReleaseMessage({
stage: 'build-start',
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:
runs-on: ubuntu-22.04
timeout-minutes: 5
needs: start-message
steps:
- name: trigger pre-release milestone check
uses: actions/github-script@v7
with:
script: | # js
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release-milestone-check.yml',
ref: '${{ github.ref }}',
inputs: {
commit: '${{ inputs.commit }}',
version: '${{ inputs.version }}',
}
});
check-version:
runs-on: ubuntu-22.04
timeout-minutes: 10
@@ -135,7 +113,7 @@ jobs:
uses: actions/github-script@v7
id: release_branch
with:
result-encoding: string
result_encoding: string
script: | # js
const { getReleaseBranch } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';

View File

@@ -1,41 +1,80 @@
name: Automatically set milestone
run-name: Set milestone for ${{ github.event.head_commit.message }}
name: Milestone Reminder
on:
push:
pull_request:
types: [closed]
branches:
- release-x.**
- "master"
jobs:
set-milestone:
name: Automatically set milestone
milestone-reminder:
if: github.event.pull_request.merged == true
name: Remind to set milestone
runs-on: ubuntu-22.04
timeout-minutes: 5
permissions:
pull-requests: write
issues: write
contents: read
issues: read
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
ref: master # we want the logic from master, even though we're triggering on a release branch
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- uses: actions/github-script@v7
with:
script: | # js
const { setMilestoneForCommits } = require('${{ github.workspace }}/release/dist/index.cjs');
const owner = context.repo.owner;
const repo = context.repo.repo;
const branchName = `${{ github.ref }}`;
const commitMessage = context?.payload?.head_commit?.message ?? '';
const pullNumber = '${{ github.event.pull_request.number }}';
await setMilestoneForCommits({
github,
// the github API doesn't expose linked issues, so we have to parse the body ourselves
function getLinkedIssueId(pull) {
// https://regexr.com/7sk94
const match = pull.data.body.match(/(close(s|d)?|fixe?(s|d)?|resolve(s|d)?) (#|https?:\/\/github.com\/.+\/issues\/)(\d+)/i);
return match && match?.[2];
}
function getMilestone(pullOrIssue) {
// pull has pull.data.milestone, issue has issue.milestone
return pullOrIssue?.data?.milestone || pullOrIssue?.milestone;
}
const pull = await github.rest.pulls.get({
owner,
repo,
branchName,
commitMessages: [commitMessage],
pull_number: pullNumber,
});
if (getMilestone(pull)) {
console.log("Pull request has milestone", pull.data.milestone?.title);
process.exit(0);
}
const issueId = getLinkedIssueId(pull);
if (issueId) {
console.log('linked issue found', issueId);
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueId,
});
const milestone = getMilestone(issue);
if (milestone) {
console.log("Linked issue has milestone", milestone.title);
process.exit(0);
}
}
console.log("No milestone found");
const author = pull.data.user.login;
const guideLink = "https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#3dea255ffa3b4f74942a227844e889fa";
const message = `@${author} Did you forget to add a milestone to the issue for this PR? _[When and where should I add a milestone?](${guideLink})_`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: message,
});

View File

@@ -1,42 +0,0 @@
name: Conventional commit PR Title Reminder
on:
pull_request:
branches:
- master
paths:
- enterprise/frontend/src/embedding-sdk/**
jobs:
pr-title-reminder:
runs-on: ubuntu-22.04
timeout-minutes: 5
permissions:
pull-requests: write
steps:
- name: Check if the PR title is already following Conventional Commits style
uses: actions/github-script@v7
id: is-conventional-commit
with:
result-encoding: string
script: | # js
const pullRequestTitle = context.payload.pull_request.title
const conventionalCommitRegex = /^(?<type>\w+)\((?<scope>\w+)\):/
const match = pullRequestTitle.match(conventionalCommitRegex)
if (match) {
const { type, scope } = match.groups
return ['feat', 'fix', 'perf', 'docs', 'style', 'refactor', 'test', 'build', 'ci'].includes(type) && scope === 'sdk'
}
- name: Checkout repository so `gh` CLI works
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Remind PR authors to use conventional commit in the title
if: ${{ steps.is-conventional-commit.outputs.result != 'true' }}
uses: actions/github-script@v7
with:
script: |
const { updateComment } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
await updateComment({github, context})

View File

@@ -1,162 +0,0 @@
name: Cross-branch migration tests
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "resources/migrations/**"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
migrations-h2-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: h2
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}
migrations-postgres-12-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
MB_DB_HOST: localhost
MB_DB_DBNAME: circle_test
MB_DB_USER: circle_test
services:
postgres:
image: postgres:12-alpine
ports:
- "5432:5432"
env:
POSTGRES_USER: circle_test
POSTGRES_DB: circle_test
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}
migrations-postgres-latest-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
MB_DB_HOST: localhost
MB_DB_DBNAME: circle_test
MB_DB_USER: circle_test
services:
postgres:
image: circleci/postgres:latest
ports:
- "5432:5432"
env:
POSTGRES_USER: circle_test
POSTGRES_DB: circle_test
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}
migrations-mysql-8-0-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
services:
mysql:
image: cimg/mysql:8.0
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}
migrations-mysql-latest-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
services:
mysql:
image: mysql:latest
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}
migrations-mariadb-10-2-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
services:
mariadb:
image: circleci/mariadb:10.2.23
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}
migrations-mariadb-latest-ee:
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
CI: 'true'
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
services:
mariadb:
image: circleci/mariadb:latest
ports:
- "3306:3306"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/cross-branch-migration-test
with:
base-ref: ${{ github.base_ref }}

View File

@@ -21,6 +21,7 @@ on:
- Athena
- BigQuery
- Druid
- Google Analytics
- Google Classpath Test
- Maria DB
- Maria DB (latest)
@@ -108,6 +109,42 @@ jobs:
junit-name: 'be-tests-druid-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-googleanalytics-ee:
if: inputs.driver == 'Google Analytics'
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
CI: 'true'
DRIVERS: googleanalytics
steps:
- uses: actions/checkout@v4
- name: Test Google Analytics driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-googleanalytics-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-google-related-drivers-classpath-test:
if: inputs.driver == 'Google Classpath Test'
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
CI: 'true'
DRIVERS: 'googleanalytics,bigquery-cloud-sdk'
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
steps:
- uses: actions/checkout@v4
- name: Test Google Related Drivers Classpath
uses: ./.github/actions/test-driver
with:
junit-name: 'be-google-related-drivers-classpath-test'
test-args: ':only "[metabase.query-processor-test.expressions-test metabase.driver.google-test metabase.driver.googleanalytics-test ${{ inputs.test }}] :times ${{ inputs.times }}"'
be-tests-mariadb-10-2-ee:
if: inputs.driver == 'Maria DB'
runs-on: ubuntu-22.04

View File

@@ -40,9 +40,6 @@ jobs:
MB_ATHENA_TEST_ACCESS_KEY: ${{ secrets.MB_ATHENA_TEST_ACCESS_KEY }}
MB_ATHENA_TEST_SECRET_KEY: ${{ secrets.MB_ATHENA_TEST_SECRET_KEY }}
MB_ATHENA_TEST_S3_STAGING_DIR: ${{ secrets.MB_ATHENA_TEST_S3_STAGING_DIR }}
# These credentials are used to test the driver when the user does not have the athena:GetTableMetadata permission
MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_ACCESS_KEY: ${{ secrets.MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_ACCESS_KEY }}
MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_SECRET_KEY: ${{ secrets.MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_SECRET_KEY }}
steps:
- uses: actions/checkout@v4
- name: Test Athena driver
@@ -129,24 +126,20 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-databricks-ee:
be-tests-googleanalytics-ee:
needs: files-changed
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
CI: 'true'
DRIVERS: databricks
MB_DATABRICKS_TEST_HOST: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HOST }}
MB_DATABRICKS_TEST_HTTP_PATH: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HTTP_PATH }}
MB_DATABRICKS_TEST_TOKEN: ${{ secrets.MB_DATABRICKS_JDBC_TEST_TOKEN }}
MB_DATABRICKS_TEST_CATALOG: 'metabase_ci'
DRIVERS: googleanalytics
steps:
- uses: actions/checkout@v4
- name: Test Databricks driver
- name: Test Google Analytics driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-ee'
junit-name: 'be-tests-googleanalytics-ee'
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
@@ -160,28 +153,27 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-druid-jdbc-ee:
be-google-related-drivers-classpath-test:
needs: files-changed
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
CI: 'true'
DRIVERS: druid-jdbc
services:
druid:
image: metabase/druid:29.0.1-json-no-ingestion
ports:
- "8888:8888"
env:
CLUSTER_SIZE: nano-quickstart
DRIVERS: 'googleanalytics,bigquery-cloud-sdk'
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
steps:
- uses: actions/checkout@v4
- name: Test Druid JDBC driver
- name: Test Google Related Drivers Classpath
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-jdbc-ee'
test-args: ":exclude-tags '[:mb/once]'"
junit-name: 'be-google-related-drivers-classpath-test'
test-args: ':only "[metabase.query-processor-test.expressions-test metabase.driver.google-test metabase.driver.googleanalytics-test]"'
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -289,7 +281,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-4-4-ee'
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -335,7 +327,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-4-4-ee'
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -369,7 +361,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-5-0-ee'
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -415,7 +407,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-5-0-ee'
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -452,39 +444,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-latest-ee'
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-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 }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mongo-sharded-cluster-ee:
needs: files-changed
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
CI: 'true'
DRIVERS: mongo
services:
mongodb:
image: metabase/mongo-sharded-cluster:6
ports:
- "27017:17017"
steps:
- uses: actions/checkout@v4
- name: Test MongoDB driver (Sharded cluster)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-sharded-cluster-ee'
test-args: ':only "[metabase.driver.mongo.sharded-cluster-test]"'
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -840,11 +800,6 @@ jobs:
MB_SNOWFLAKE_TEST_WAREHOUSE: ${{ secrets.MB_SNOWFLAKE_TEST_WAREHOUSE }}
MB_SNOWFLAKE_TEST_PK_USER: METABASE PK
MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY }}
# RSA Role testing:
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DEFAULT_USER: RSA_ROLE_TEST_DEFAULT_USER
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_CUSTOM_USER: RSA_ROLE_TEST_CUSTOM_USER
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_ROLE: RSA_ROLE_TEST_ROLE
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DB: RSA_ROLE_TEST_DB
steps:
- uses: actions/checkout@v4
- name: Test Snowflake driver
@@ -926,7 +881,7 @@ jobs:
be-tests-sqlserver-2017-ee:
needs: files-changed
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
CI: 'true'

View File

@@ -2,31 +2,12 @@ name: E2E Cross-version Tests
on:
schedule:
- cron: '0 9 * * 1-5'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
- cron: '0 9 * * 0'
jobs:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_cross_version: ${{ steps.changes.outputs.e2e_cross_version }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
cross-version-testing:
needs: files-changed
if: ${{ needs.files-changed.outputs.e2e_cross_version == 'true' || github.event_name == 'schedule'}}
runs-on: ubuntu-22.04
timeout-minutes: 20
timeout-minutes: 60
strategy:
matrix:
version: [
@@ -71,12 +52,11 @@ jobs:
- name: Wait for Metabase to start on port 3000
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Run Cypress on the source
run: |
yarn cypress run \
--browser ${{ steps.cypress-prep.outputs.chrome-path }} \
--browser chrome \
--config-file e2e/test/scenarios/cross-version/source/shared/cross-version-source.config.js \
--spec e2e/test/scenarios/cross-version/source/**/*.cy.spec.js
- name: Stop Metabase ${{ matrix.version.source }}
@@ -104,7 +84,7 @@ jobs:
- name: Run Cypress on the target
run: |
yarn cypress run \
--browser ${{ steps.cypress-prep.outputs.chrome-path }} \
--browser chrome \
--config-file e2e/test/scenarios/cross-version/target/shared/cross-version-target.config.js \
--spec e2e/test/scenarios/cross-version/target/**/*.cy.spec.js
- name: Upload Cypress Artifacts upon failure
@@ -117,11 +97,20 @@ jobs:
if-no-files-found: ignore
notify-on-failure:
needs: cross-version-testing
if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }}
if: ${{ always() && contains(needs.*.result, 'failure') }}
name: Cross-version test failure
runs-on: ubuntu-22.04
timeout-minutes: 5
env:
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
AUTHOR_NAME: ${{ github.event.workflow_run.head_commit.author.name }}
steps:
- name: Generate job summary
run: |
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:" >> $GITHUB_STEP_SUMMARY
echo "${{ github.event.workflow_run.html_url }}" >> $GITHUB_STEP_SUMMARY
- uses: actions/setup-node@v4
with:
node-version: lts/Hydrogen # 18.x.x
@@ -130,8 +119,11 @@ jobs:
uses: actions/github-script@v7
with:
script: | # js
const FAILED_RUN_URL = "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}";
const REF_NAME = "${{ github.ref_name }}";
const FAILED_RUN_URL = "${{ github.event.workflow_run.html_url }}";
const FAILED_RUN_NAME = "${{ github.event.workflow_run.name }}";
const BREAKING_COMMIT = "${{ github.event.workflow_run.head_sha }}";
const AUTHOR = process.env.AUTHOR_NAME;
const BRANCH = process.env.BRANCH_NAME;
// notify slack of failure
const { WebClient } = require('@slack/web-api');
@@ -145,7 +137,7 @@ jobs:
"type": "header",
"text": {
"type": "plain_text",
"text": `:broken_heart: Cross-version tests are failing on ${REF_NAME}`,
"text": `:broken_heart: Cross-version tests are failing on ${BRANCH}`,
"emoji": true,
}
},
@@ -157,7 +149,7 @@ jobs:
"type": "section",
"text": {
"type": "mrkdwn",
"text": `See <${FAILED_RUN_URL}|failing run details>.`
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on the <https://github.com/metabase/metabase/commits/${BRANCH}|\`${BRANCH}\`> branch.`
}
},
]

View File

@@ -20,15 +20,6 @@ on:
type: string
required: false
default: "ee"
qa_db:
description: 'Select the type of Database you want to run your tests with'
type: choice
required: true
default: "none"
options:
- none
- sql
- mongo
jobs:
workflow-summary:
@@ -44,7 +35,6 @@ jobs:
echo '- `spec`: ${{ inputs.spec }}' >> $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 '' >> $GITHUB_STEP_SUMMARY
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
@@ -54,8 +44,8 @@ jobs:
name: Stress test E2E flake fix
env:
DISPLAY: ""
QA_DB_ENABLED: ${{ contains(fromJSON('["sql", "mongo"]'), inputs.qa_db) }}
CYPRESS_QA_DB_MONGO: ${{ inputs.qa_db == 'mongo' }}
QA_DB_ENABLED: true
CYPRESS_REPLAYIO_ENABLED: 1
CYPRESS_ALL_FEATURES_TOKEN: ${{ secrets.ENTERPRISE_TOKEN }}
CYPRESS_NO_FEATURES_TOKEN: ${{ secrets.E2E_STARTER_TOKEN }}
MB_SNOWPLOW_AVAILABLE: true
@@ -76,9 +66,9 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
maildev: true
openldap: true
postgres: ${{ inputs.qa_db == 'sql' }}
mysql: ${{ inputs.qa_db == 'sql' }}
mongo: ${{ inputs.qa_db == 'mongo' }}
postgres: true
mysql: true
mongo: true
- name: Download Metabase ${{ matrix.edition }} uberjar
uses: ./.github/actions/e2e-download-uberjar
@@ -93,7 +83,6 @@ jobs:
java-version: 11
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Run Snowplow micro
uses: ./.github/actions/run-snowplow-micro
@@ -106,7 +95,7 @@ jobs:
--spec '${{ github.event.inputs.spec }}' \
--env burn=${{ github.event.inputs.burn_in }},grep='${{ github.event.inputs.grep }}' \
--config-file e2e/support/cypress-stress-test.config.js \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
--browser 'replay-chromium'
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4

View File

@@ -4,7 +4,7 @@ on:
push:
branches:
- "master"
- "release-**"
- 'release-**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
@@ -39,39 +39,8 @@ jobs:
id: e2e-matrix
uses: ./.github/actions/build-e2e-matrix
# if this is a test on a release branch, we need to check the build requirements
get-build-requirements:
if: |
!cancelled() &&
contains(github.ref, 'release-x')
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get build dependencies
uses: actions/github-script@v7
id: dependencies
with:
script: | # js
const { getBuildRequirements, getVersionFromReleaseBranch } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = getVersionFromReleaseBranch('${{ github.ref }}');
const requirements = getBuildRequirements(version);
return {
java_version: requirements.java,
node_version: requirements.node,
};
build:
needs: [files-changed, e2e-matrix-builder, get-build-requirements]
needs: [files-changed, e2e-matrix-builder]
if: |
!cancelled() &&
github.event.pull_request.draft == false &&
@@ -85,19 +54,14 @@ jobs:
env:
MB_EDITION: ${{ matrix.edition }}
INTERACTIVE: false
# make sure that builds on release branches get licenses, because we use them for releases
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
with:
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: uberjar
java-version: "${{ needs.get-build-requirements.outputs.java_version || 11 }}"
m2-cache-key: e2e-tests
- name: Build uberjar with ./bin/build.sh
run: ./bin/build.sh
@@ -123,11 +87,14 @@ jobs:
CYPRESS_ALL_FEATURES_TOKEN: ${{ secrets.ENTERPRISE_TOKEN }}
CYPRESS_NO_FEATURES_TOKEN: ${{ secrets.E2E_STARTER_TOKEN }}
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number || '' }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.even.head.sha }}
COMMIT_INFO_MESSAGE : ${{ github.event.pull_request.title || github.event.head_commit.message || github.even.head.sha }}
MB_SNOWPLOW_AVAILABLE: true
MB_SNOWPLOW_URL: "http://localhost:9090" # Snowplow micro
CYPRESS_REPLAYIO_ENABLED: 1
RECORD_REPLAY_METADATA_FILE: /tmp/replay-metadata.json
RECORD_REPLAY_METADATA_TEST_RUN_ID: ${{ github.run_id }}
REPLAY_API_KEY: ${{ secrets.REPLAY_IO_TOKEN }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.e2e-matrix-builder.outputs.matrix) }}
@@ -145,7 +112,6 @@ jobs:
postgres: ${{ matrix.name != 'mongo'}}
mysql: ${{ matrix.name != 'mongo'}}
mongo: ${{ matrix.name == 'mongo'}}
webhook: true
- name: Retrieve uberjar artifact for ${{ matrix.edition }}
uses: actions/download-artifact@v4
@@ -167,7 +133,6 @@ jobs:
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Run Snowplow micro
@@ -178,24 +143,24 @@ jobs:
run: |
yarn run test-cypress-run \
--env grepTags=@OSS,grepOmitFiltered=true \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
--spec './e2e/test/scenarios/**/*.cy.spec.js' \
--browser 'replay-chromium'
- name: Run slow and resource-intensive Cypress tests
if: matrix.name == 'slow' && github.event_name != 'schedule'
run: |
yarn run test-cypress-run \
--env grepTags="@slow",grepOmitFiltered=true \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
--spec './e2e/test/scenarios/**/*.cy.spec.js' \
--browser 'replay-chromium'
- name: Run Flaky Cypress tests
if: matrix.name == 'flaky' && github.event_name != 'schedule'
run: |
yarn run test-cypress-run \
--env grepTags="@flaky --@quarantine",grepOmitFiltered=true \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
--spec './e2e/test/scenarios/**/*.cy.spec.js' \
--browser 'replay-chromium'
- name: Run EE Cypress tests on ${{ matrix.name }}
if: matrix.context == 'folder' && github.event_name != 'schedule'
@@ -203,33 +168,18 @@ jobs:
yarn run test-cypress-run \
--env grepTags="-@slow+-@mongo+-@flaky --@quarantine",grepOmitFiltered=true \
--folder ${{ matrix.name }} \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
--browser 'replay-chromium'
- name: Run E2E tests that depend on Mongo
if: matrix.name == 'mongo'
run: |
yarn run test-cypress-run \
--env grepTags="@mongo --@quarantine",grepOmitFiltered=true \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
--spec './e2e/test/scenarios/**/*.cy.spec.js' \
--browser 'replay-chromium'
env:
CYPRESS_QA_DB_MONGO: true
- name: Run Embedding SDK Storybook
if: matrix.context == 'embedding-sdk' && github.event_name != 'schedule'
uses: ./.github/actions/run-embedding-sdk-storybook
- name: Run E2E tests for Embedding SDK
if: matrix.context == 'embedding-sdk' && github.event_name != 'schedule'
run: |
yarn run test-cypress-run \
--env grepTags="-@slow+-@flaky --@quarantine",grepOmitFiltered=true \
--folder 'embedding-sdk' \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
env:
CYPRESS_IS_EMBEDDING_SDK: true
shell: bash
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -252,21 +202,6 @@ jobs:
./logs/test.log
if-no-files-found: ignore
- name: Publish Summary
if: failure()
uses: actions/github-script@v7
with:
script: | #js
const {
parseReport,
formatSummary
} = require("./.github/scripts/handle-mochawesome-report.js");
const report = parseReport();
const summary = formatSummary(report);
await core.summary.addRaw(summary).write();
e2e-tests-skipped-stub:
needs: [e2e-tests, e2e-matrix-builder]
if: |
@@ -282,10 +217,46 @@ jobs:
- run: |
echo "Didn't run due to conditional filtering"
pr-env:
visual-regression-tests:
runs-on: ubuntu-22.04
timeout-minutes: 60
needs: [build]
if: |
!cancelled() &&
contains(github.event.pull_request.labels.*.name, 'PR-Env')
uses: ./.github/workflows/pr-env.yml
secrets: inherit
!cancelled() && needs.build.result == 'success'
name: percy-visual-regression-tests
steps:
- uses: actions/checkout@v4
- name: Prepare Docker containers
uses: ./.github/actions/e2e-prepare-containers
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
maildev: true
- name: Retrieve uberjar artifact for ${{ matrix.edition }}
uses: actions/download-artifact@v4
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
- name: Get the version info
run: |
jar xf target/uberjar/metabase.jar version.properties
mv version.properties resources/
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare JDK 11
uses: actions/setup-java@v4
with:
java-version: 11
distribution: 'temurin'
- name: Prepare Cypress environment
uses: ./.github/actions/prepare-cypress
- name: Percy Test
run: yarn run test-visual-run
env:
PERCY_TOKEN: ${{ github.ref == 'refs/heads/master' && secrets.PERCY_TOKEN || ''}}

View File

@@ -1,39 +0,0 @@
name: Embedding SDK
on:
push:
branches:
- "master"
- "release-**"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
sdk-build:
if: contains(github.event.pull_request.labels.*.name, 'embedding-sdk-build')
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cljs"
- name: Build SDK bundle
run: yarn run build-embedding-sdk
- name: Generate SDK package.json
run: yarn run embedding-sdk:generate-package "$GITHUB_SHA"
- name: Generate .npmrc for Github npm registry
uses: actions/setup-node@v4
with:
registry-url: "https://npm.pkg.github.com"
- name: Publish package
run: yarn run embedding-sdk:publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,34 +0,0 @@
name: Flake Issue Creator
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * 1-5' # every weekday at 1am US eastern time
jobs:
flake-issue-creator:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn --frozen-lockfile && yarn build
- name: Update flake issues
uses: actions/github-script@v7
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const { updateFlakeIssues } = require('${{ github.workspace }}/release/dist/index.cjs');
if (!process.env.SLACK_BOT_TOKEN) {
throw new Error('SLACK_BOT_TOKEN is required');
}
await updateFlakeIssues({
github,
owner: context.repo.owner,
repo: context.repo.repo,
});

View File

@@ -1,35 +0,0 @@
name: Flake Status Report
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * 2,5' # Tuesdays and Fridays at 1am US eastern time
jobs:
flake-status-report:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn --frozen-lockfile && yarn build
- name: Post Flake Status Report
uses: actions/github-script@v7
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const { summarizeFlakes } = require('${{ github.workspace }}/release/dist/index.cjs');
if (!process.env.SLACK_BOT_TOKEN) {
throw new Error('SLACK_BOT_TOKEN is required');
}
await summarizeFlakes({
github,
owner: context.repo.owner,
repo: context.repo.repo,
channelName: 'engineering-ci'
});

View File

@@ -4,15 +4,15 @@ on:
workflow_dispatch:
inputs:
spec:
description: "Relative path of the target spec"
description: 'Relative path of the target spec'
type: string
required: true
burn_in:
description: "Number of times to run the test (e.g. 20)"
description: 'Number of times to run the test (e.g. 20)'
type: string
required: true
grep:
description: "Grep and filter tests to run in isolation"
description: 'Grep and filter tests to run in isolation'
type: string
required: false
@@ -45,12 +45,10 @@ jobs:
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cljs"
- name: Build cljs
run: yarn build:cljs
- name: Stress-test ${{ github.event.inputs.spec }} ${{ github.event.inputs.burn_in }} times
run: |
for i in {1..${{ github.event.inputs.burn_in }}}; do
yarn jest --runInBand --ci --silent --no-cache \
yarn test-unit \
--testPathPattern '${{ github.event.inputs.spec }}' \
--testNamePattern '${{ github.event.inputs.grep }}'
if [[ "$?" != 0 ]]; then

View File

@@ -70,7 +70,7 @@ jobs:
fe-tests-unit:
needs: files-changed
if: github.event.pull_request.draft == false && needs.files-changed.outputs.frontend_all == 'true'
runs-on: ubuntu-22.04
runs-on: buildjet-2vcpu-ubuntu-2204
timeout-minutes: 60
env:
JEST_JUNIT_OUTPUT_DIR: ./target/junit
@@ -91,11 +91,10 @@ jobs:
run: yarn run test-unit --silent
- name: Upload coverage to codecov.io
if: github.ref_name == 'master'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: front-end
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()

View File

@@ -1,69 +0,0 @@
name: Update Loki Snapshots
on:
pull_request:
types: [labeled]
jobs:
check-label-and-update-snapshot:
if: contains(github.event.pull_request.labels.*.name, 'loki-update')
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
ref: ${{ github.head_ref }}
- name: Prepare frontend environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cljs"
- name: Compile CLJS
run: yarn build-pure:cljs
- name: Build Storybook
# We won't be able to use Storybook play's `userEvent` with production build
# see https://github.com/storybookjs/storybook/issues/19758
run: NODE_ENV=development yarn build-storybook
- name: Serve Storybook
run: yarn http-server storybook-static -p 6006 &
shell: bash
- name: Wait for Storybook to become available
run: |
echo "Waiting for Storybook to be served..."
while ! curl --silent --fail http://localhost:6006; do
sleep 5
echo "Retrying..."
done
echo "Storybook is being served!"
- name: Update Loki Snapshots
run: yarn test-visual:loki-update
- name: Commit and Push Changes
run: |
git config --global user.name "Metabase Automation"
git config --global user.email "github-automation@metabase.com"
git add .loki/**
git commit -m "Update Loki Snapshots"
git push
env:
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Remove 'loki-update' label
uses: actions/github-script@v7
with:
github-token: ${{secrets.METABASE_AUTOMATION_USER_TOKEN}}
script: |
github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'loki-update'
})

View File

@@ -1,87 +0,0 @@
name: Loki Visual Regression Testing
on: [pull_request]
jobs:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
frontend_ci: ${{ steps.changes.outputs.frontend_ci }}
frontend_sources: ${{ steps.changes.outputs.frontend_sources }}
frontend_loki_ci: ${{ steps.changes.outputs.frontend_loki_ci }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
visual-test:
needs: files-changed
if: needs.files-changed.outputs.frontend_ci == 'true' || needs.files-changed.outputs.frontend_sources == 'true' || needs.files-changed.outputs.frontend_loki_ci == 'true'
runs-on: ubuntu-latest
services:
docker:
image: docker:19.03.12
options: --privileged
ports:
- 2376:2376
env:
DOCKER_TLS_CERTDIR: /certs
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /tmp/docker-certs:/certs/client
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Prepare frontend environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "cljs"
- name: Compile CLJS
run: yarn build-pure:cljs
- name: Build Storybook
# We won't be able to use Storybook play's `userEvent` with production build
# see https://github.com/storybookjs/storybook/issues/19758
run: NODE_ENV=development yarn build-storybook
- name: Serve Storybook
run: yarn http-server storybook-static -p 6006 &
shell: bash
- name: Wait for Storybook to become available
run: |
echo "Waiting for Storybook to be served..."
while ! curl --silent --fail http://localhost:6006; do
sleep 5
echo "Retrying..."
done
echo "Storybook is being served!"
- name: Run Loki Visual Tests
run: |
yarn test-visual:loki
- name: Generate Visual Report on Failure
if: failure()
run: yarn test-visual:loki-report
- name: Upload Artifact
id: artifact-upload-step
if: failure()
uses: actions/upload-artifact@v4
with:
name: loki-report
path: .loki/
if-no-files-found: ignore

View File

@@ -1,32 +0,0 @@
name: Update changelog for MBQL library changes
on:
pull_request:
types:
- opened
- reopened
- labeled
- unlabeled
- synchronize
paths:
- src/metabase/lib/js.cljs
jobs:
mbql-library-changelog-check:
runs-on: ubuntu-latest
if: ${{ !contains( github.event.pull_request.labels.*.name, '.metabase-lib/no-change' ) }}
steps:
# - uses: jesusvasquez333/verify-pr-label-action@v1.4.0
# with:
# github-token: '${{ secrets.GITHUB_TOKEN }}'
# valid-labels: '.metabase-lib/no-change'
# disable-reviews: true
- uses: actions/checkout@v4
- name: Check for updated MBQL library changelog
run: |
if [[ $(git show HEAD -- docs/developers-guide/mbql-library-changelog.md) ]]; then
echo "Library and changelog updated together - all good"
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."
exit 1
fi

View File

@@ -1,4 +1,4 @@
name: Lint migrations
name: Migrations
on:
push:
@@ -9,14 +9,14 @@ on:
jobs:
lint-migrations:
check-migrations:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: lint-migrations
- name: Verify Liquibase Migrations
run: ./bin/lint-migrations-file.sh
- uses: actions/checkout@v4
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: check-migrations
- name: Verify Liquibase Migrations
run: ./bin/lint-migrations-file.sh

View File

@@ -1,48 +0,0 @@
name: A New Feature Slack Notification
on:
issues:
types:
- closed
jobs:
on-epic-closed:
runs-on: ubuntu-22.04
timeout-minutes: 5
if: ${{ contains(github.event.issue.labels.*.name, '.Epic') || contains(github.event.issue.labels.*.name, 'Type:New Feature') }}
steps:
- name: Setting title
uses: actions/github-script@v6
id: vars
with:
script: |
core.setOutput('issue_title', ${{ toJson(github.event.issue.title) }}.replaceAll(/"/g, '\\"'));
- name: Extracting team name
id: team_name
run: |
team_label=$(echo "${{ join(github.event.issue.labels.*.name, ',') }}" | grep -o '\.Team/[^,]*' | head -n 1)
echo $team_label
echo "team_label=${team_label}" >> "$GITHUB_OUTPUT"
team_name=${team_label#*.Team/}
echo $team_name
echo "team_name=${team_name^^}_SLACK_EPIC_WEBHOOK_URL" >> "$GITHUB_OUTPUT"
- name: Send the issue ${{ github.event.issue.number }} to Slack
id: slack
if: ${{ steps.team_name.outputs.team_label }}
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.vars.outputs.issue_title }}\n${{ github.event.issue.html_url }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets[steps.team_name.outputs.team_name] }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -0,0 +1,91 @@
# Triggers Percy job by "@metabase-bot run visual tests" comment in a PR
name: PercyIssueComment
on:
issue_comment:
types: [created]
jobs:
pr_info:
if: github.event.comment.body == '@metabase-bot run visual tests'
runs-on: ubuntu-22.04
outputs:
pull_request_number: ${{ fromJson(steps.fetch_pr.outputs.data).head.number }}
branch_name: ${{ fromJson(steps.fetch_pr.outputs.data).head.ref }}
commit_sha: ${{ fromJson(steps.fetch_pr.outputs.data).head.sha }}
steps:
- uses: tibdex/github-app-token@v2.1.0
id: generate-token
with:
app_id: ${{ secrets.METABASE_BOT_APP_ID }}
private_key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
ref: ${{ needs.pr_info.outputs.branch_name }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Verify Membership
id: verify-membership
uses: ./.github/actions/verify-membership
with:
github_token: ${{ steps.generate-token.outputs.token }}
organization_name: metabase
username: ${{ github.event.comment.user.login }}
- run: |
result=${{ steps.verify-membership.outputs.is_member }}
if [ $result == false ]; then
echo User ${{ github.event.comment.user.login }} is not a member of Metabase organization
exit 1
fi
- name: Fetch issue
uses: octokit/request-action@v2.x
id: fetch_issue
with:
route: GET ${{ github.event.issue.url }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch PR
uses: octokit/request-action@v2.x
id: fetch_pr
with:
route: GET ${{ fromJson(steps.fetch_issue.outputs.data).pull_request.url }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
percy:
timeout-minutes: 30
runs-on: buildjet-4vcpu-ubuntu-2004
needs: [pr_info]
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MB_EDITION: ee
CYPRESS_ALL_FEATURES_TOKEN: ${{ secrets.ENTERPRISE_TOKEN }}
CYPRESS_NO_FEATURES_TOKEN: ${{ secrets.E2E_STARTER_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.pr_info.outputs.branch_name }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: percy-issue-comment
- name: Prepare cypress environment
uses: ./.github/actions/prepare-cypress
- run: ./bin/build.sh
- name: Get the version info
run: |
jar xf target/uberjar/metabase.jar version.properties
mv version.properties resources/
- name: Run maildev
run: docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev:2.0.5
- name: Percy Test
run: yarn run test-visual-run
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_COMMIT: ${{ needs.pr_info.outputs.commit_sha }}
PERCY_BRANCH: ${{ needs.pr_info.outputs.branch_name }}
PERCY_PULL_REQUEST: ${{ needs.pr_info.outputs.pull_request_number }}

View File

@@ -0,0 +1,39 @@
# Triggers Percy job on pull requests that contain "visual" label
name: Percy visual tests
on:
pull_request:
types: [synchronize, labeled]
jobs:
percy:
if: contains(github.event.pull_request.labels.*.name, 'visual')
timeout-minutes: 30
runs-on: buildjet-4vcpu-ubuntu-2004
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MB_EDITION: ee
CYPRESS_ALL_FEATURES_TOKEN: ${{ secrets.ENTERPRISE_TOKEN }}
CYPRESS_NO_FEATURES_TOKEN: ${{ secrets.E2E_STARTER_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: percy-visual-label
- name: Prepare cypress environment
uses: ./.github/actions/prepare-cypress
- run: ./bin/build.sh
- name: Get the version info
run: |
jar xf target/uberjar/metabase.jar version.properties
mv version.properties resources/
- name: Run maildev
run: docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev:2.0.5
- name: Percy Test
run: yarn run test-visual-run
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

View File

@@ -1,17 +0,0 @@
name: Perf test command
on:
pull_request:
types: [ labeled ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
run-perf-test:
if: ${{ github.event.label.name == 'Run-perf' }}
uses: ./.github/workflows/perf-test.yml
with:
wait_for_uberjar: true
secrets: inherit

View File

@@ -1,150 +0,0 @@
name: CI for Perf tests
run-name: Run perf test for ${{ github.ref_name }} by @${{ github.actor }}
on:
workflow_call:
inputs:
wait_for_uberjar:
description: "Wait for uberjar build"
required: false
type: boolean
jobs:
build:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
name: Build Metabase Docker image
timeout-minutes: 60
permissions:
id-token: write
contents: read
actions: read
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
with:
registries: "${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}"
- name: Wait for uberjar
id: wait_for_uberjar
if: ${{ inputs.wait_for_uberjar == true }}
run: |
## Get workflow run id for uberjar build
curl -Ls --output e2e-tests.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/e2e-tests.yml/runs?head_sha=${{ github.event.pull_request.head.sha || github.sha }}
ID=$(jq -r '.workflow_runs[0].id' e2e-tests.json)
## Wait for uberjar build to complete
while [ true ]; do
curl -Ls --output uberjar.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/actions/runs/${ID}/jobs?filter=latest
jq -r '.jobs[] | select(.name == "build (ee)") | .steps[] | select(.name == "Prepare uberjar artifact") | .status' uberjar.json | grep -q "completed" && break
echo "Waiting for uberjar build..."
sleep 10
done
echo "run_id=$(jq -r '.workflow_runs[0].id' e2e-tests.json)" >> $GITHUB_OUTPUT
- name: Retrieve uberjar artifact for ee
uses: actions/download-artifact@v4
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ inputs.wait_for_uberjar && steps.wait_for_uberjar.outputs.run_id || github.run_id }}
- name: Move uberjar to bin/docker
run: |
jar xf target/uberjar/metabase.jar
mv target/uberjar/metabase.jar bin/docker/metabase.jar
- name: Build container
uses: docker/build-push-action@v6
with:
context: bin/docker/
platforms: linux/amd64
network: host
tags: ${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/metabase-enterprise:pr${{ github.event.number }}
push: true
launch_perf_test:
needs: [ build ]
runs-on: ubuntu-latest
name: Launch Perf Test
permissions:
id-token: write
contents: read
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
version: 1.50.1
sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576
- name: Create OIDC Token
id: create-oidc-token
shell: bash
run: |
export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${{ secrets.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
- name: Setup Kube Context
uses: azure/k8s-set-context@v2
with:
method: kubeconfig
kubeconfig: |
kind: Config
apiVersion: v1
current-context: default
clusters:
- name: default
cluster:
certificate-authority-data: ${{ secrets.PR_ENV_K8S_CERTIFICATE_AUTHORITY_DATA }}
server: ${{ secrets.PR_ENV_K8S_SERVER }}
users:
- name: oidc-token
user:
token: ${{ steps.create-oidc-token.outputs.IDTOKEN }}
contexts:
- name: default
context:
cluster: default
namespace: default
user: oidc-token
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Download Deployment YAML template
run: aws s3 cp s3://metabase-pr-env/perf-test-pr.yml.tmpl ./perf-test-pr.yml.tmpl
- name: Trim SHA
env:
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
id: split
run: echo "::set-output name=fragment::${SHA:5}"
- name: Render Deployment YAML
uses: nowactions/envsubst@v1
with:
input: ./perf-test-pr.yml.tmpl
output: ./perf-test-pr.yml
env:
IMAGE_TAG: pr${{ github.event.number }}
PR_NUMBER: ${{ github.event.number }}
RUN_ID: ${{ steps.split.outputs.fragment }}
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
TEST_NAME: test
- name: Deploy PR Review ENV
run: |
kubectl apply -f ./perf-test-pr.yml

View File

@@ -1,15 +0,0 @@
name: PR Closed or Unlabeled
on:
pull_request:
types: [ closed, unlabeled ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
pr-env:
if: ${{ github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env') || github.event.label.name == 'PR-Env' }}
uses: ./.github/workflows/pr-env-destroy.yml
secrets: inherit

View File

@@ -1,71 +0,0 @@
name: CI for PR Review ENV Destroy
run-name: Destroying Dynamic PR Environment for ${{ github.ref_name }} by @${{ github.actor }}
on:
workflow_call:
jobs:
destroy_pr:
runs-on: ubuntu-latest
name: PR Review ENV Destroy
permissions:
id-token: write
contents: read
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
version: 1.50.1
sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576
- name: Create OIDC Token
id: create-oidc-token
shell: bash
run: |
export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${{ secrets.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
- name: Setup Kube Context
uses: azure/k8s-set-context@v2
with:
method: kubeconfig
kubeconfig: |
kind: Config
apiVersion: v1
current-context: default
clusters:
- name: default
cluster:
certificate-authority-data: ${{ secrets.PR_ENV_K8S_CERTIFICATE_AUTHORITY_DATA }}
server: ${{ secrets.PR_ENV_K8S_SERVER }}
users:
- name: oidc-token
user:
token: ${{ steps.create-oidc-token.outputs.IDTOKEN }}
contexts:
- name: default
context:
cluster: default
namespace: default
user: oidc-token
- name: Destroy PR Review ENV
run: |
kubectl delete metabase -n hosting-pr${{ github.event.number }} hosting-pr${{ github.event.number }}
kubectl delete ns hosting-pr${{ github.event.number }}
- name: Setup psql client
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
- name: Drop app database if exists
run: |
PGPASSWORD='${{ secrets.PR_ENV_DB_PASSWORD }}' \
psql \
-h ${{ secrets.PR_ENV_DB_HOST }} \
-U ${{ secrets.PR_ENV_DB_USER }} \
-d ${{ secrets.PR_ENV_DB_NAME }} \
-c "DROP DATABASE IF EXISTS hosting_pr${{ github.event.number }};"

View File

@@ -1,17 +0,0 @@
name: PR-Env labeled
on:
pull_request:
types: [ labeled ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
pr-env:
if: ${{ github.event.label.name == 'PR-Env' }}
uses: ./.github/workflows/pr-env.yml
with:
wait_for_uberjar: true
secrets: inherit

View File

@@ -1,169 +0,0 @@
name: CI for PR Review ENV
run-name: Building Dynamic PR Environment for ${{ github.ref_name }} by @${{ github.actor }}
on:
workflow_call:
inputs:
wait_for_uberjar:
description: "Wait for uberjar build"
required: false
type: boolean
jobs:
build:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
name: Build Metabase Docker image
timeout-minutes: 60
permissions:
id-token: write
contents: read
actions: read
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
with:
registries: "${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}"
- name: Wait for uberjar
id: wait_for_uberjar
if: ${{ inputs.wait_for_uberjar == true }}
run: |
## Get workflow run id for uberjar build
curl -Ls --output e2e-tests.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/e2e-tests.yml/runs?head_sha=${{ github.event.pull_request.head.sha || github.sha }}
ID=$(jq -r '.workflow_runs[0].id' e2e-tests.json)
## Wait for uberjar build to complete
while [ true ]; do
curl -Ls --output uberjar.json \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/actions/runs/${ID}/jobs?filter=latest
jq -r '.jobs[] | select(.name == "build (ee)") | .steps[] | select(.name == "Prepare uberjar artifact") | .status' uberjar.json | grep -q "completed" && break
echo "Waiting for uberjar build..."
sleep 10
done
echo "run_id=$(jq -r '.workflow_runs[0].id' e2e-tests.json)" >> $GITHUB_OUTPUT
- name: Retrieve uberjar artifact for ee
uses: actions/download-artifact@v4
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ inputs.wait_for_uberjar && steps.wait_for_uberjar.outputs.run_id || github.run_id }}
- name: Move uberjar to bin/docker
run: |
jar xf target/uberjar/metabase.jar
mv target/uberjar/metabase.jar bin/docker/metabase.jar
- name: Build container
uses: docker/build-push-action@v6
with:
context: bin/docker/
platforms: linux/amd64
network: host
tags: ${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/metabase-enterprise:pr${{ github.event.number }}
push: true
deploy_pr:
needs: [ build ]
runs-on: ubuntu-latest
name: PR Review ENV
permissions:
id-token: write
contents: read
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
version: 1.50.1
sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576
- name: Create OIDC Token
id: create-oidc-token
shell: bash
run: |
export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${{ secrets.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
- name: Setup Kube Context
uses: azure/k8s-set-context@v2
with:
method: kubeconfig
kubeconfig: |
kind: Config
apiVersion: v1
current-context: default
clusters:
- name: default
cluster:
certificate-authority-data: ${{ secrets.PR_ENV_K8S_CERTIFICATE_AUTHORITY_DATA }}
server: ${{ secrets.PR_ENV_K8S_SERVER }}
users:
- name: oidc-token
user:
token: ${{ steps.create-oidc-token.outputs.IDTOKEN }}
contexts:
- name: default
context:
cluster: default
namespace: default
user: oidc-token
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Setup psql client
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
- name: Create app database if not exists
run: |
export PGPASSWORD='${{ secrets.PR_ENV_DB_PASSWORD }}'
export PGUSER=${{ secrets.PR_ENV_DB_USER }}
export PGHOST=${{ secrets.PR_ENV_DB_HOST }}
export PGDATABASE=${{ secrets.PR_ENV_DB_NAME }}
psql -tc "SELECT 1 FROM pg_database WHERE datname = 'hosting_pr${{ github.event.number }}'" \
| grep -q 1 \
|| psql -c "CREATE DATABASE hosting_pr${{ github.event.number }}"
- name: Download Deployment YAML template
run: aws s3 cp s3://metabase-pr-env/metabase.yml.tmpl ./metabase.yml.tmpl
- name: Render Deployment YAML
uses: nowactions/envsubst@v1
with:
input: ./metabase.yml.tmpl
output: ./metabase.yml
env:
IMAGE_TAG: pr${{ github.event.number }}
PR_NUMBER: ${{ github.event.number }}
MB_PREMIUM_EMBEDDING_TOKEN: ${{ secrets.PR_ENV_MB_PREMIUM_EMBEDDING_TOKEN }}
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy PR Review ENV
run: |
kubectl apply -f ./metabase.yml
preview_links:
runs-on: ubuntu-latest
needs: [ deploy_pr ]
steps:
- uses: marocchino/sticky-pull-request-comment@v2
with:
recreate: true
message: |
👋 Deploying a preview environment for commit ${{ github.event.pull_request.head.sha || github.sha }}.
✅ Preview:
https://pr${{ github.event.number }}.coredev.metabase.com
🗒️ [login instructions](https://www.notion.so/metabase/Ephemeral-PR-Environments-c74a3710c87a460dbf4fc0007a001458)

View File

@@ -5,16 +5,12 @@ on:
workflow_dispatch:
inputs:
version:
description: "Metabase version (e.g. v0.46.3)"
description: 'Metabase version (e.g. v0.46.3)'
type: string
required: true
commit:
description: "A full-length commit SHA-1 hash"
description: 'A full-length commit SHA-1 hash'
required: true
auto:
description: 'auto-patch release DO NOT SET MANUALLY'
type: boolean
default: false
jobs:
release-artifact:
@@ -95,26 +91,26 @@ jobs:
edition: [oss, ee]
java-version: [11, 17]
steps:
- name: Prepare JRE (Java Run-time Environment)
uses: actions/setup-java@v4
with:
java-package: jre
java-version: ${{ matrix.java-version }}
distribution: "temurin"
- run: java -version
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact
with:
name: metabase-${{ matrix.edition }}-uberjar
- name: Launch uberjar (and keep it running)
run: java -jar ./metabase.jar &
- name: Wait for Metabase to start
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
- name: Prepare JRE (Java Run-time Environment)
uses: actions/setup-java@v4
with:
java-package: jre
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
- run: java -version
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact
with:
name: metabase-${{ matrix.edition }}-uberjar
- name: Launch uberjar (and keep it running)
run: java -jar ./metabase.jar &
- name: Wait for Metabase to start
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
run-sanity-check:
runs-on: ubuntu-22.04
timeout-minutes: 30
needs: [release-artifact]
needs: [release-artifact, check-uberjar-health]
strategy:
matrix:
edition: [oss, ee]
@@ -123,63 +119,59 @@ jobs:
DISPLAY: ""
CYPRESS_ALL_FEATURES_TOKEN: ${{ secrets.ENTERPRISE_TOKEN }}
CYPRESS_NO_FEATURES_TOKEN: ${{ secrets.E2E_STARTER_TOKEN }}
# disabled because of out of memory issues
# probably related to https://github.com/cypress-io/cypress/issues/27415
CYPRESS_NO_COMMAND_LOG: 1
MB_SNOWPLOW_AVAILABLE: true
MB_SNOWPLOW_URL: "http://localhost:9090" # Snowplow micro
TERM: xterm
steps:
- name: Check out the code
uses: actions/checkout@v4
with:
ref: ${{ needs.release-artifact.outputs.commit }}
- name: Prepare Docker containers
uses: ./.github/actions/e2e-prepare-containers
with:
services:
maildev:
image: maildev/maildev:2.0.5
ports:
- "1080:1080"
- "1025:1025"
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
maildev: true
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare JDK 11
uses: actions/setup-java@v4
with:
java-version: 11
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Run Snowplow micro
uses: ./.github/actions/run-snowplow-micro
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact for ${{ matrix.edition }}
with:
name: metabase-${{ matrix.edition }}-uberjar
- name: Get the version info
run: |
jar xf metabase.jar version.properties
mv version.properties resources/
- name: Run a few important Cypress tests as sanity check
run: |
mkdir -p ./target/uberjar && cp metabase.jar ./target/uberjar/metabase.jar
yarn test-cypress-run \
--env grepTags="@smoke",grepOmitFiltered=true \
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
steps:
- name: Check out the code
uses: actions/checkout@v4
with:
ref: ${{ needs.release-artifact.outputs.commit }}
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare JDK 11
uses: actions/setup-java@v4
with:
java-version: 11
distribution: "temurin"
- name: Prepare Cypress environment
uses: ./.github/actions/prepare-cypress
- name: Run Snowplow micro
uses: ./.github/actions/run-snowplow-micro
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact for ${{ matrix.edition }}
with:
name: metabase-${{ matrix.edition }}-uberjar
- name: Get the version info
run: |
jar xf metabase.jar version.properties
mv version.properties resources/
- name: Run a few important Cypress tests as sanity check
run: |
mkdir -p ./target/uberjar && cp metabase.jar ./target/uberjar/metabase.jar
yarn test-cypress-run --folder onboarding
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-failed-tests-recording
path: |
./cypress
./logs/test.log
if-no-files-found: ignore
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-failed-tests-recording
path: |
./cypress
./logs/test.log
if-no-files-found: ignore
containerize:
needs: [release-artifact]
needs: [release-artifact, run-sanity-check]
runs-on: ubuntu-22.04
timeout-minutes: 15
strategy:
@@ -193,71 +185,71 @@ jobs:
outputs:
image_tag: ${{ steps.target.outputs.image_tag }}
steps:
- name: Get the version
run: |
if [[ "${{ matrix.edition }}" == "ee" ]]; then
echo "VERSION=${{ needs.release-artifact.outputs.ee_version }}" >> $GITHUB_ENV
else
echo "VERSION=${{ needs.release-artifact.outputs.oss_version }}" >> $GITHUB_ENV
fi
- name: Check out the code
uses: actions/checkout@v4
with:
ref: ${{ needs.release-artifact.outputs.commit }}
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact
with:
name: metabase-${{ matrix.edition }}-uberjar
- name: Move the Uberjar to the context dir
run: mv metabase.jar bin/docker/.
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
driver-opts: network=host
- name: Build ${{ matrix.edition }} container
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64
network: host
tags: localhost:5000/local-metabase:${{ env.VERSION }}
no-cache: true
push: true
- name: Get the version
run: |
if [[ "${{ matrix.edition }}" == "ee" ]]; then
echo "VERSION=${{ needs.release-artifact.outputs.ee_version }}" >> $GITHUB_ENV
else
echo "VERSION=${{ needs.release-artifact.outputs.oss_version }}" >> $GITHUB_ENV
fi
- name: Check out the code
uses: actions/checkout@v4
with:
ref: ${{ needs.release-artifact.outputs.commit }}
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact
with:
name: metabase-${{ matrix.edition }}-uberjar
- name: Move the Uberjar to the context dir
run: mv metabase.jar bin/docker/.
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
driver-opts: network=host
- name: Build ${{ matrix.edition }} container
uses: docker/build-push-action@v3
with:
context: bin/docker/.
platforms: linux/amd64
network: host
tags: localhost:5000/local-metabase:${{ env.VERSION }}
no-cache: true
push: true
- name: Launch container
run: docker run --rm -dp 3000:3000 localhost:5000/local-metabase:${{ env.VERSION }}
timeout-minutes: 5
- name: Wait for Metabase to start
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
timeout-minutes: 3
- name: Launch container
run: docker run --rm -dp 3000:3000 localhost:5000/local-metabase:${{ env.VERSION }}
timeout-minutes: 5
- name: Wait for Metabase to start
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
timeout-minutes: 3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
- name: Retag and push container image to ${{ vars.DOCKERHUB_OWNER }} staging Docker Hub repo
id: target
run: |
# SHA-1 truncated to 7 digits should be enough
SHORT_HASH=${COMMIT:0:7}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
- name: Retag and push container image to ${{ vars.DOCKERHUB_OWNER }} staging Docker Hub repo
id: target
run: |
# SHA-1 truncated to 7 digits should be enough
SHORT_HASH=${COMMIT:0:7}
IMAGE_TAG=$VERSION-$SHORT_HASH
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
IMAGE_TAG=$VERSION-$SHORT_HASH
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "Retag local image for staging"
docker tag localhost:5000/local-metabase:$VERSION $CONTAINER_IMAGE
echo "Retag local image for staging"
docker tag localhost:5000/local-metabase:$VERSION $CONTAINER_IMAGE
echo "Pushing container image $CONTAINER_IMAGE ..."
docker push $CONTAINER_IMAGE
echo "Pushing container image $CONTAINER_IMAGE ..."
docker push $CONTAINER_IMAGE
echo "Finished!"
shell: bash
env:
COMMIT: ${{ needs.release-artifact.outputs.commit }}
echo "Finished!"
shell: bash
env:
COMMIT: ${{ needs.release-artifact.outputs.commit }}
verify-docker-pull:
runs-on: ubuntu-22.04
@@ -269,80 +261,22 @@ jobs:
env:
IMAGE_TAG: ${{ needs.containerize.outputs.image_tag }}
steps:
- name: Login to Docker Hub # authenticated, to avoid being rate-throttled
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
- name: Pull the container image
run: |
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
echo "Pulling container image $CONTAINER_IMAGE ..."
docker pull $CONTAINER_IMAGE
echo "Successful!"
- name: Launch container
run: |
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
docker run --rm -dp 3000:3000 $CONTAINER_IMAGE
timeout-minutes: 5
- name: Wait for Metabase to start
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
timeout-minutes: 3
trigger-auto-publish:
if: ${{ inputs.auto }}
needs: [run-sanity-check, check-uberjar-health, verify-docker-pull]
permissions:
actions: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: trigger auto publish
uses: actions/github-script@v7
with:
script: | # js
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release.yml',
ref: '${{ github.ref }}',
inputs: {
commit: '${{ inputs.commit }}',
version: '${{ inputs.version }}',
auto: true,
}
});
tests-complete-message:
if: always()
runs-on: ubuntu-22.04
needs: [run-sanity-check, check-uberjar-health, verify-docker-pull]
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Send tests complete message
uses: actions/github-script@v7
env:
SLACK_RELEASE_CHANNEL: ${{ vars.SLACK_RELEASE_CHANNEL }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const { sendTestsCompleteMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
const allJobsSuccessful =
'${{ needs.run-sanity-check.result }}' === 'success' &&
'${{ needs.check-uberjar-health.result }}' === 'success' &&
'${{ needs.verify-docker-pull.result }}' === 'success';
await sendTestsCompleteMessage({
owner: context.repo.owner,
repo: context.repo.repo,
version: '${{ inputs.version }}',
runId: '${{ github.run_id }}',
channelName: '${{ vars.SLACK_RELEASE_CHANNEL }}',
testStatus: allJobsSuccessful ? 'success' : 'failure',
}).catch(console.error);
- name: Login to Docker Hub # authenticated, to avoid being rate-throttled
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
- name: Pull the container image
run: |
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
echo "Pulling container image $CONTAINER_IMAGE ..."
docker pull $CONTAINER_IMAGE
echo "Successful!"
- name: Launch container
run: |
CONTAINER_IMAGE=${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_STAGING_REPO }}:$IMAGE_TAG
docker run --rm -dp 3000:3000 $CONTAINER_IMAGE
timeout-minutes: 5
- name: Wait for Metabase to start
run: while ! curl -s 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
timeout-minutes: 3

View File

@@ -1,8 +1,3 @@
# 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:
@@ -10,10 +5,10 @@ on:
push:
branches:
- master
- "release**"
- "feature**"
- 'release**'
- 'feature**'
tags:
- "**"
- '**'
jobs:
files-changed:
@@ -57,7 +52,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
path: presto-kerberos-docker
- name: Bring up Presto+Kerberos cluster
run: cd presto-kerberos-docker && docker compose up -d && cd ..
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
@@ -68,7 +63,6 @@ jobs:
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@v4
with:
@@ -80,30 +74,11 @@ jobs:
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@v7
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
run: /home/runner/work/metabase/metabase/mba-src/src/main.clj --mb . --data-db postgres-data -n example.com up
- name: Run test script in MBA instance
run: docker compose -f mba-compose.yml exec metabase .github/scripts/run-presto-kerberos-integration-test.sh
run: >
mba --mb . --data-db postgres-data -n example.com \
run .github/scripts/run-presto-kerberos-integration-test.sh

View File

@@ -1,254 +0,0 @@
name: Release Metabase Embedding SDK for React
on:
workflow_dispatch:
inputs:
# TODO: Add a version validation, so the workflow won't fail when publishing to npm
sdk_version:
description: 'SDK version (e.g. 0.1.3)'
type: string
required: true
git_ref:
description: 'A git commit hash that will be used to build the SDK'
type: string
required: true
concurrency:
# We want to ensure only one job is running at a time because
# there could be a conflict when updating the readme file.
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
check-git-tag:
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
tag: embedding-sdk-${{ inputs.sdk_version }}
steps:
- name: Check out the code using the provided ref
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref }}
fetch-depth: 0
fetch-tags: true
- name: check tags
run: |
if [ $(git tag -l | grep $tag$) ]
then
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow again."
exit 1
else
exit 0
fi
test:
needs: check-git-tag
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Check out the code using the provided ref
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref }}
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "release-sdk"
- name: Run unit tests
run: yarn embedding-sdk:test-unit
build-sdk:
needs: test
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Check out the code using the provided ref
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref }}
fetch-depth: 0
fetch-tags: true
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
with:
m2-cache-key: "release-sdk"
- name: Update readme
run: |
./bin/embedding-sdk/release_utils.bash update_readme ${{ inputs.sdk_version }}
- name: Bump published npm package version
# NOTE: this should happen before "Build SDK bundle" as we inject SDK version into the code during build step
run: |
./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ inputs.sdk_version }}
- name: Update changelog
run: |
yarn embedding-sdk:generate-changelog -o changelog-diff
- name: Build SDK bundle
run: yarn run build-embedding-sdk
- name: Generate SDK package.json in the build directory
run: yarn run embedding-sdk:generate-package
- name: Upload built SDK package as artifact
uses: actions/upload-artifact@v4
with:
name: metabase-sdk
path: ./resources/embedding-sdk
- name: Upload changelog diff
uses: actions/upload-artifact@v4
with:
name: sdk-changelog-diff
path: ./changelog-diff
build-jar:
needs: test
uses: ./.github/workflows/uberjar-sdk.yml
with:
git_ref: ${{ inputs.git_ref }}
image_name: embedding-sdk-${{ inputs.sdk_version }}
secrets: inherit
upload-jar:
needs: build-jar
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- uses: actions/download-artifact@v4
name: Retrieve uberjar artifact
with:
name: metabase-ee-uberjar
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_S3_RELEASE_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_S3_RELEASE_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Upload uberjar to S3
env:
BUCKET: ${{ vars.AWS_S3_DOWNLOADS_BUCKET }}
BUCKET_PATH: sdk/v${{ inputs.sdk_version }}/metabase.jar
FILE: ./target/uberjar/metabase.jar
shell: bash
run: |
aws s3 cp $FILE s3://$BUCKET/$BUCKET_PATH
publish-npm:
needs: [build-sdk, upload-jar]
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Check out master to prepare for a SDK version bump PR
uses: actions/checkout@v4
with:
# when we created version update PR, it would only have diff from 2 files SDK readme and SDK package.json template
ref: master
- name: Setup git user
run: |
git config --global user.email "github-automation@metabase.com"
git config --global user.name "Metabase Automation"
- name: Update readme
run: |
bash ./bin/embedding-sdk/release_utils.bash update_readme ${{ inputs.sdk_version }}
- name: Bump published npm package version
run: |
bash ./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ inputs.sdk_version }}
- name: Retrieve SDK changelog diff
uses: actions/download-artifact@v4
with:
name: sdk-changelog-diff
- name: Update changelog
run: |
cat changelog-diff enterprise/frontend/src/embedding-sdk/CHANGELOG.md > new-changelog
mv new-changelog enterprise/frontend/src/embedding-sdk/CHANGELOG.md
- name: Create a PR updating readme + published version, and changelog (using GitHub Metabase Automation account)
run: |
git checkout -b update-sdk-version-${{ inputs.sdk_version }}
git commit -a -m 'Update Readme version references and published npm version to ${{ inputs.sdk_version }}'
git push origin HEAD
gh pr create --base master\
--assignee "${GITHUB_ACTOR}"\
--title "Update SDK version to ${{ inputs.sdk_version }}"\
--body "Update Readme version references and published npm package version to ${{ inputs.sdk_version }}"
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- name: Edit PR adding useful information (using Metabase bot app)
run: |
gh pr edit --add-reviewer @metabase/embedding --add-label no-backport
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Auto approve PR (using GitHub Actions account)
run: |
gh pr review --approve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Pull request auto-merge (using GitHub Metabase Automation account)
run: |
gh pr merge --auto --squash
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Retrieve build SDK package artifact
uses: actions/download-artifact@v4
with:
name: metabase-sdk
path: sdk
- name: Publish to NPM
run: |
cd sdk
echo //registry.npmjs.org/:_authToken=${{ secrets.NPM_RELEASE_TOKEN }} > .npmrc
npm publish
git-tag:
needs: publish-npm
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
tag: embedding-sdk-${{ inputs.sdk_version }}
steps:
- name: Check out the code using the provided ref
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref }}
# Lightweight tags don't need committer name and email
- name: Create a new git tag
run: |
git tag ${{ env.tag }}
- name: Push the new tag
id : push-tag
run: |
git push origin ${{ env.tag }}

View File

@@ -1,58 +0,0 @@
name: Update Release Log
run-name: Update Release Log ${{ inputs.version }}
on:
workflow_dispatch:
inputs:
version:
description: 'Major Metabase version (e.g. 45, 52, 68)'
type: number
required: true
schedule:
- cron: '45 * * * *' # hourly
workflow_call:
inputs:
version:
description: 'Major Metabase version (e.g. 45, 52, 68)'
type: number # needs to be a number to pass variables
required: true
jobs:
update-release-log:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
VERSION: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')
&& inputs.version || vars.CURRENT_VERSION }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_S3_RELEASE_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_S3_RELEASE_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- uses: actions/checkout@v4
with:
fetch-depth: 0 # we want all branches and tags
- name: Install Dependencies
run: yarn --cwd release --frozen-lockfile && npm i -g tsx
- name: generate release Log
run: cd release && tsx ./src/release-log-run.ts $VERSION > v$VERSION.html
- name: generate release channel log
run: cd release && tsx ./src/release-channel-log.ts > channels.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
- name: upload release channel log to the web
run: |
aws s3 cp \
release/channels.html \
s3://${{ vars.AWS_S3_STATIC_BUCKET }}/release-log/channels.html
- name: Create cloudfront invalidation
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ vars.AWS_CLOUDFRONT_STATIC_ID }} \
--paths "/release-log/v$VERSION.html" "/release-log/channels.html"

View File

@@ -1,53 +0,0 @@
name: Release Milestone Check
run-name: Pre-release Milestone check for ${{ 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:
check-milestone:
permissions: write-all
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: yarn --cwd release --frozen-lockfile && yarn --cwd release build
- uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
SLACK_RELEASE_CHANNEL: ${{ vars.SLACK_RELEASE_CHANNEL }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const {
checkMilestoneForRelease,
sendMilestoneCheckMessage,
} = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
const commitHash = '${{ inputs.commit }}';
const issueCount = await checkMilestoneForRelease({
github,
owner: context.repo.owner,
repo: context.repo.repo,
version,
commitHash,
});
await sendMilestoneCheckMessage({
channelName: process.env.SLACK_RELEASE_CHANNEL,
issueCount,
version,
});

View File

@@ -1,55 +0,0 @@
name: Release Notes Preview
run-name: Preview ${{ inputs.version }} release notes
on:
workflow_dispatch:
inputs:
version:
description: 'Metabase version (e.g. v0.46.3)'
type: string
required: true
jobs:
preview-release-notes:
runs-on: ubuntu-22.04
timeout-minutes: 5
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@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get Release Version
uses: actions/github-script@v7
env:
DOCKERHUB_OWNER: test_owner
DOCKERHUB_REPO: test_repo
AWS_S3_DOWNLOADS_BUCKET: test_bucket
with:
script: | # js
const { getChangelog } = require('${{ github.workspace }}/release/dist/index.cjs');
const fs = require('fs');
const version = '${{ inputs.version }}';
const notes = await getChangelog({
version,
github,
owner: context.repo.owner,
repo: context.repo.repo,
});
fs.writeFileSync(`release-notes-preview-${version}.md`, notes);
- name: Upload Release Notes
uses: actions/upload-artifact@v4
with:
name: release-notes-preview-${{inputs.version}}.md
path: |
${{ github.workspace }}/release-notes-preview-*.md

Some files were not shown because too many files have changed in this diff Show More