Compare commits

..

1 Commits

Author SHA1 Message Date
Emmad Usmani
b4452fb699 scatter plot scale setting 2024-05-07 10:50:00 -07:00
9085 changed files with 391785 additions and 963354 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,157 +14,18 @@
: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-mb-driver-tests {:level :warning}
:metabase/validate-mb-once {: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.models.notification/create-notification!
metabase.pulse.send/send-pulse!
metabase.query-processor.store/store-database!
;; Safe in tests, where we typically use a private, temporary index per thread.
metabase.search.core/reindex!
metabase.util/run-count!
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
:hive-like
:mongo
:oracle
:presto-jdbc
:redshift
:snowflake
:sparksql
:sqlite
:sqlserver
:vertica}}
:metabase/defsetting-must-specify-export {:level :warning}
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests {:level :warning}
:metabase/mbql-query-first-arg {:level :warning}
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning}
:metabase/validate-deftest {:level :warning}
:metabase/validate-logging {:level :warning}
:metabase/warn-about-schema= {:level :warning}
;;
;; disabled linters
@@ -190,7 +50,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)
@@ -244,16 +104,13 @@
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"}}
:discouraged-namespace
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
cheshire.core {:message "Use metabase.util.json instead of cheshire.core directly."}
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+."}
@@ -265,197 +122,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.channel.render.core
metabase.channel.template.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
metabase.email.result-attachment} ; 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.notification #{metabase.notification.core
metabase.notification.payload.core}
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.core}
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
;; deprecating naked metabase.search namespace in favor of this.
metabase.search.core
metabase.search.spec}
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.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.public-settings}
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.notification :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.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
@@ -480,11 +147,16 @@
clojure.tools.namespace.parse ns.parse
colorize.core colorize
environ.core env
hf.depstar.api depstar
hiccup.core hiccup
hiccup.util hiccup.util
honey.sql sql
honey.sql.helpers sql.helpers
java-time.api t
honeysql.core hsql
honeysql.format hformat
honeysql.helpers hh
honeysql.types htypes
java-time t
macaw.core macaw
malli.core mc
malli.error me
@@ -497,10 +169,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.analysis.native-query-analyzer nqa
metabase.analysis.native-query-analyzer.parameter-substitution nqa.sub
metabase.analyze.query-results qr
metabase.analyze.classifiers.category classifiers.category
metabase.analyze.classifiers.core classifiers
@@ -551,11 +220,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.channel.render.core channel.render
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 +236,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 +247,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
@@ -595,7 +271,6 @@
metabase.driver.sql-jdbc.test-util sql-jdbc.tu
metabase.driver.sql.parameters.substitute sql.params.substitute
metabase.driver.sql.parameters.substitution sql.params.substitution
metabase.eid-translation eid-translation
metabase.email-test et
metabase.email.messages messages
metabase.formatter formatter
@@ -611,7 +286,6 @@
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
@@ -619,26 +293,26 @@
metabase.models.interface mi
metabase.models.moderation-review moderation-review
metabase.models.native-query-snippet native-query-snippet
metabase.models.notification models.notification
metabase.models.params params
metabase.models.permissions perms
metabase.models.permissions-group perms-group
metabase.models.permissions-group-membership perms-group-membership
metabase.models.permissions-revision perms-revision
metabase.models.permissions.parse perms-parse
metabase.models.pulse models.pulse
metabase.models.pulse pulse
metabase.models.pulse-channel pulse-channel
metabase.models.pulse-channel-recipient pulse-channel-recipient
metabase.models.query.permissions query-perms
metabase.models.setting.cache setting.cache
metabase.models.timeline timeline
metabase.models.timeline-event timeline-event
metabase.notification.core notification
metabase.notification.test-util notification.tu
metabase.native-query-analyzer query-analyzer
metabase.native-query-analyzer.parameter-substitution nqa.sub
metabase.plugins.initialize plugins.init
metabase.public-settings public-settings
metabase.public-settings.premium-features premium-features
metabase.pulse.core pulse
metabase.pulse pulse ; NB some conflicts with metabase.models.pulse
metabase.pulse.markdown markdown
metabase.pulse.render render
metabase.pulse.render.body body
metabase.pulse.render.style style
@@ -671,9 +345,8 @@
metabase.related related
metabase.search.config search.config
metabase.search.filter search.filter
metabase.search.in-place.filter search.in-place.filter
metabase.search.in-place.util search.util
metabase.search.in-place.scoring scoring
metabase.search.util search.util
metabase.search.scoring scoring
metabase.server.middleware.auth mw.auth
metabase.server.middleware.browser-cookie mw.browser-cookie
metabase.server.middleware.exceptions mw.exceptions
@@ -686,6 +359,8 @@
metabase.server.middleware.ssl mw.ssl
metabase.server.request.util req.util
metabase.server.protocols server.protocols
metabase.shared.util shared.u
metabase.shared.util.currency currency
metabase.sync.analyze.fingerprint sync.fingerprint
metabase.sync.field-values field-values
metabase.sync.interface i
@@ -706,8 +381,10 @@
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.currency currency
metabase.util.date-2 u.date
metabase.util.date-2.common u.date.common
metabase.util.date-2.parse u.date.parse
@@ -715,7 +392,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
@@ -724,12 +400,10 @@
metabase.util.macros u.macros
metabase.util.malli mu
metabase.util.malli.schema ms
metabase.util.markdown markdown
metabase.util.password u.password
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
@@ -743,117 +417,106 @@
toucan.models models}}}
: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.impl.http-test/with-captured-http-requests clojure.core/fn
metabase.channel.impl.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.send-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.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.namespaces/import-fns potemkin/import-vars
metabase.util.ssh/with-ssh-tunnel clojure.core/let
metabase.xrays.domain-entities.malli/defn schema.core/defn
metabuild-common.core/with-open-jar-file-system clojure.core/let
metabuild-common.files/with-open-jar-file-system clojure.core/let
monger.operators/defoperator clojure.core/def
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
@@ -861,39 +524,33 @@
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.channel.template.handlebars-test/with-temp-template! hooks.common/with-vec-first-binding
metabase.channel.template.handlebars-helper/defhelper hooks.metabase.channel.template.handlebars-helper/defhelper
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.oracle-test/with-temp-user hooks.common/let-one-with-optional-value
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.datasets/test-driver hooks.metabase.test.data.datasets/test-driver
metabase.test.data.datasets/test-drivers hooks.metabase.test.data.datasets/test-drivers
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
@@ -901,6 +558,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*
@@ -911,71 +569,68 @@
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/test-drivers hooks.common/do*
metabase.test/run-mbql-query hooks.metabase.test.data/mbql-query
metabase.test/test-driver hooks.metabase.test.data.datasets/test-driver
metabase.test/test-drivers hooks.metabase.test.data.datasets/test-drivers
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/format-color hooks.metabase.util/format-color
metabase.util.log/trace hooks.metabase.util.log/info
metabase.util.log/tracef hooks.metabase.util.log/infof
metabase.util.log/debug hooks.metabase.util.log/info
metabase.util.log/debugf 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/warn hooks.metabase.util.log/info
metabase.util.log/warnf 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.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.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
metabase.xrays.domain-entities.malli/define-getters-and-setters macros.metabase.xrays.domain-entities.malli/define-getters-and-setters}}
{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-enterprise.advanced-permissions.api.util-test/with-impersonations! macros.metabase-enterprise.advanced-permissions.api.util-test/with-impersonations!
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}}}
@@ -987,14 +642,14 @@
;; 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}
@@ -1005,19 +660,7 @@
;; `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
@@ -1025,9 +668,7 @@
{: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}}}
:metabase/defsetting-must-specify-export {:level :off}}
:hooks
{:analyze-call
{clojure.core/defmacro hooks.clojure.core/non-thread-safe-form-should-end-with-exclamation
@@ -1077,11 +718,6 @@
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}}}}
qp-and-driver-source-namespaces
{:linters
{:discouraged-namespace
@@ -1095,8 +731,4 @@
{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/reducible-query {:message "Don't use application database directly in QP code; use QP Store/metadata provider"}}}}}}

View File

@@ -0,0 +1,205 @@
(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-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}]
(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,242 @@
(ns hooks.clojure.test
(:require
[clj-kondo.hooks-api :as hooks]
[clojure.string :as str]
[hooks.common]))
(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.pulse/send-pulse!
metabase.query-processor.store/store-database!
next.jdbc/execute!})
(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 (hooks.common/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})

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

@@ -0,0 +1,402 @@
(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)
(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 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"))

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

@@ -0,0 +1,12 @@
(ns hooks.metabase.lib.test-util.macros
(:require [clj-kondo.hooks-api :as hooks]))
(defn with-testing-against-standard-queries [{{[_ & args] :children} :node}]
(let [[query-sym & body] args
node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[query-sym (hooks/token-node 'nil)])
body))]
{:node node*}))

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
@@ -162,6 +164,7 @@
token-status
toucan-name
uncached-setting
uploads-table-prefix
user-visibility
version
version-info
@@ -181,13 +184,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 +245,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
;;

View File

@@ -0,0 +1,60 @@
(ns hooks.metabase.test.util
(:require [clj-kondo.hooks-api :as hooks]
[hooks.common :as common]))
(defn- namespaced-symbol-node? [node]
(when (hooks/token-node? node)
(let [symb (hooks/sexpr node)]
(and (symbol? symb)
(namespace symb)))))
(defn with-temporary-setting-values
"Rewrite a form like
(with-temporary-setting-values [x 1, some.ns/y 2]
...)
as one like
(let [_ 1, _ some.ns/y, _ 2]
...)
for analysis purposes. We only need to 'capture' namespaced Setting bindings with a `_` so Kondo considers their
namespace to be 'used' and to catch undefined var usage."
[{:keys [node]}]
(let [{[_ bindings & body] :children} node
bindings (if (hooks/vector-node? bindings)
(hooks/vector-node (into []
(mapcat (fn [[setting-name v]]
(concat
[(with-meta (hooks/token-node '_) (meta setting-name)) v]
;; if the setting name is namespace-qualified add a `_`
;; entry for it too.
(when (namespaced-symbol-node? setting-name)
[(with-meta (hooks/token-node '_) (meta setting-name)) setting-name]))))
(partition 2 (:children bindings))))
bindings)]
{:node (-> (list*
(hooks/token-node 'let)
bindings
body)
(with-meta (meta body))
hooks/list-node
(with-meta (meta body)))}))
(defn with-temp-file
[{{[_ {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 filename]]
[binding (or (when (and filename
(not (nil? (hooks/sexpr filename))))
filename)
(hooks/string-node "filename"))])))
bindings))
body))]
{:node node*}))

View File

@@ -0,0 +1,157 @@
(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- 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))
(let [[y] (:children x)]
(when (and (api/token-node? y)
(= (hooks.common/node->qualified-symbol y) 'clojure.core/format))
(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))
(api/string-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)
(comment
;; should fail, missing a format arg
(infof
{:node (api/parse-string
(str '(log/warnf "Migration lock was acquired after %d retries.")))})
(infof
{:node (api/parse-string
(str '(log/warnf e "Migration lock was acquired after %d retries.")))})
;; should fail, too many format args
(infof
{:node (api/parse-string
(str '(log/warnf "Migration lock was acquired after %d retries." 1 2)))})
(infof
{:node (api/parse-string
(str '(log/warnf e "Migration lock was acquired after %d retries." 1 2)))})
(infof
{:node (api/parse-string
(str '(log/warnf "Migration lock was acquired after {0} retries." 1)))})
;; should fail, has format args but is warn rather than warnf
(info
{:node (api/parse-string
(str '(log/warn e "Migration lock was acquired after %d retries." 1 2)))})
;; should fail -- should not use i18n/tru or i18n/trs
(infof
{:node (api/parse-string
(str '(log/warnf (metabase.util.i18n/tru "Migration lock was acquired after {0} retries." 1))))})
(info
{:node (api/parse-string
(str '(log/warn (metabase.util.i18n/trs "Migration lock was acquired after {0} retries." 1))))}))

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

@@ -0,0 +1,8 @@
(ns macros.metabase.shared.util.namespaces)
(defmacro import-fn
"Kondo macro replacement for [[metabase.shared.util.namespaces/import-fn]]."
([sym]
`(def ~(-> sym name symbol) "docstring" ~sym))
([sym name]
`(def ~name "docstring" ~sym)))

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,8 +0,0 @@
(ns macros.metabase.util.namespaces)
(defmacro import-fn
"Kondo macro replacement for [[metabase.util.namespaces/import-fn]]."
([sym]
`(def ~(-> sym name symbol) "docstring" ~sym))
([sym name]
`(def ~name "docstring" ~sym)))

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,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,329 +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.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/send-notifications!
metabase.pulse.send/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- explicitly-safe? [qualified-symbol]
(contains? symbols-allowed-in-fns-not-ending-in-an-exclamation-point qualified-symbol))
(defn- explicitly-unsafe? [config qualified-symbol]
(contains? (get-in config [:linters :metabase/validate-deftest :parallel/unsafe]) qualified-symbol))
(defn- unsafe? [config qualified-symbol]
(and (or (end-with-exclamation? qualified-symbol)
(explicitly-unsafe? config qualified-symbol))
(not (explicitly-safe? qualified-symbol))))
(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)))
(check-node [form]
(when-let [qualified-symbol (hooks.common/node->qualified-symbol form)]
(when (unsafe? config 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)))))]
(walk check-node 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,447 +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)
#_{:clj-kondo/ignore [:discouraged-var]}
(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 with-vec-first-binding
"For macros like
(with-temp-file [binding \"temp.txt\" \"content\"]
(slurp binding))
=>
(let [binding nil]
(slurp binding))
where the first arg is a vector of bindings."
[{{[_ {[x & _args] :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 node->qualified-symbol
"If the node is a symbol, return it as a qualified symbol if it's not already. Otherwise return nil."
[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,26 +0,0 @@
(ns hooks.metabase.channel.template.handlebars-helper
(:require
[clj-kondo.hooks-api :as hooks]
[hooks.common]))
(defn defhelper
"defhelper hook"
[{:keys [node]}]
(let [[_ helper-name description argvec & body] (:children node)
helper-fn-name (symbol (str (hooks/sexpr helper-name) "*"))]
{:node (hooks/list-node
(list (hooks/token-node 'do)
(hooks/list-node
(list (hooks/token-node 'defn)
(hooks/token-node helper-fn-name)
description
argvec
(hooks/list-node body)))
(hooks/list-node
(list (hooks/token-node 'def)
helper-name
description
(hooks/list-node
(into [(hooks/token-node 'fn)
argvec]
body))))))}))

View File

@@ -1,12 +0,0 @@
(ns hooks.metabase.lib.test-util.macros
(:require [clj-kondo.hooks-api :as hooks]))
(defn with-testing-against-standard-queries [{{[_ & args] :children} :node}]
(let [[query-sym & body] args
node* (hooks/list-node
(list*
(hooks/token-node 'let)
(hooks/vector-node
[query-sym (hooks/token-node 'nil)])
body))]
{:node node*}))

View File

@@ -1,56 +0,0 @@
(ns hooks.metabase.test.data.datasets
(:require [clj-kondo.hooks-api :as hooks]
[clojure.set :as set]))
(defn- only-core-drivers?
"Whether we're for sure only testing against the 'core' drivers that are also app DB types: `:postgres`, `:mysql`, or
`:h2`. Tests like these are ok to mark `:mb/once` or `:mb/driver-tests` since we always run the full test suite
against app DB drivers anyway."
[node]
(let [drivers (hooks/sexpr (second (:children node)))
drivers (cond
(keyword? drivers) #{drivers}
(set? drivers) drivers
:else nil)]
(when drivers
(empty? (set/difference drivers #{:h2 :mysql :postgres})))))
(defn- validate-mb-once [x]
(when (:mb/once (meta (:ns x)))
(hooks/reg-finding!
(assoc (meta (:ns x))
:message (str "You should not use test-driver or test-drivers in a namespace marked ^:mb/once"
" [:metabase/validate-mb-once]")
:type :metabase/validate-mb-once))))
(defn- validate-mb-driver-tests [x]
(when-not (:mb/driver-tests (meta (:ns x)))
(hooks/reg-finding!
(assoc (meta (:ns x))
:message (str "Namespaces that run driver tests (test-driver or test-drivers) should be marked"
" ^:mb/driver-tests [:metabase/validate-mb-driver-tests]")
:type :metabase/validate-mb-driver-tests))))
(defn- validate-ns-tags [x]
(when-not (only-core-drivers? (:node x))
(validate-mb-once x)
(validate-mb-driver-tests x)))
(defn test-drivers [x]
(validate-ns-tags x)
(letfn [(update-node [node]
(let [[test-drivers drivers-expr & body] (:children node)]
(-> (hooks/list-node
(list*
(hooks/token-node 'do)
test-drivers
(-> drivers-expr
(vary-meta update :clj-kondo/ignore #(conj (vec %) :unused-value)))
body))
(with-meta (meta node)))))]
(update x :node update-node)))
(defn test-driver
"We can use the same hook for `test-driver` as we use for `test-drivers`."
[x]
(test-drivers x))

View File

@@ -1,60 +0,0 @@
(ns hooks.metabase.test.util
(:require [clj-kondo.hooks-api :as hooks]
[hooks.common :as common]))
(defn- namespaced-symbol-node? [node]
(when (hooks/token-node? node)
(let [symb (hooks/sexpr node)]
(and (symbol? symb)
(namespace symb)))))
(defn with-temporary-setting-values
"Rewrite a form like
(with-temporary-setting-values [x 1, some.ns/y 2]
...)
as one like
(let [_ 1, _ some.ns/y, _ 2]
...)
for analysis purposes. We only need to 'capture' namespaced Setting bindings with a `_` so Kondo considers their
namespace to be 'used' and to catch undefined var usage."
[{:keys [node]}]
(let [{[_ bindings & body] :children} node
bindings (if (hooks/vector-node? bindings)
(hooks/vector-node (into []
(mapcat (fn [[setting-name v]]
(concat
[(with-meta (hooks/token-node '_) (meta setting-name)) v]
;; if the setting name is namespace-qualified add a `_`
;; entry for it too.
(when (namespaced-symbol-node? setting-name)
[(with-meta (hooks/token-node '_) (meta setting-name)) setting-name]))))
(partition 2 (:children bindings))))
bindings)]
{:node (-> (list*
(hooks/token-node 'let)
bindings
body)
(with-meta (meta body))
hooks/list-node
(with-meta (meta body)))}))
(defn with-temp-file
[{{[_ {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 filename]]
[binding (or (when (and filename
(not (nil? (hooks/sexpr filename))))
filename)
(hooks/string-node "filename"))])))
bindings))
body))]
{:node node*}))

View File

@@ -1,124 +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,55 +0,0 @@
(ns hooks.clojure.test-test
(:require
[clj-kondo.hooks-api :as api]
[clj-kondo.impl.utils]
[clojure.edn :as edn]
[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]))
driver-modules (->> (slurp "modules/drivers/deps.edn")
edn/read-string
:deps
vals
(keep (comp keyword :local/root))
(into #{}))]
(doseq [driver driver-modules]
(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"
".clj-kondo/src"
".clj-kondo/test"
#_"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.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/trsn [[:default]]
metabase.util.i18n/tru [[:default]]
metabase.util.i18n/trun [[:default]]
metabase.util.jvm/auto-retry [[:block 1]]
metabase.util.jvm/varargs [[:block 1]]
metabase.util.jvm/with-us-locale [[:block 0]]
metabase.util.malli/disable-enforcement [[:block 0]]
metabase.util.malli/with [[:default]]
metabase.util.malli/with-api-error-message [[:default]]
metabase.util.namespaces/import-fns [[:default]]
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,67 +20,67 @@
(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 'mr/def '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)

View File

@@ -62,13 +62,6 @@ module.exports = {
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,
@@ -85,10 +78,9 @@ module.exports = {
"react/forbid-component-props": [2, { forbid: ["sx"] }],
"react-hooks/exhaustive-deps": [
"warn",
{ additionalHooks: "(useSafeAsyncFunction)" },
{ additionalHooks: "(useSyncedQueryString|useSafeAsyncFunction)" },
],
"prefer-const": [1, { destructuring: "all" }],
"no-restricted-globals": ["error", "close"],
"no-useless-escape": 0,
"no-only-tests/no-only-tests": [
"error",
@@ -141,8 +133,6 @@ module.exports = {
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:depend/recommended",
"plugin:storybook/recommended",
],
settings: {
"import/internal-regex": "^metabase/|^metabase-lib/",
@@ -170,17 +160,6 @@ module.exports = {
rules: {
"no-unconditional-metabase-links-render": "error",
"no-literal-metabase-strings": "error",
"depend/ban-dependencies": [
"error",
{
allowed: [
"underscore",
"moment",
"lodash.orderby",
"lodash.debounce",
],
},
],
},
},
{
@@ -210,13 +189,7 @@ module.exports = {
"@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/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
@@ -235,9 +208,8 @@ module.exports = {
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react",
"plugin:jest-formatting/recommended",
],
plugins: ["jest", "jest-dom", "testing-library", "jest-formatting"],
plugins: ["jest", "jest-dom", "testing-library"],
files: [
"*.unit.spec.ts",
"*.unit.spec.tsx",
@@ -248,41 +220,5 @@ module.exports = {
"jest/valid-title": ["error", { ignoreTypeOfDescribeName: true }],
},
},
{
// Enable jest formatting for cypress tests too, the plugin logic just works
extends: ["plugin:jest-formatting/recommended"],
files: ["*.cy.spec.ts", "*.cy.spec.js"],
},
{
files: ["frontend/src/**/*"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/mb-base-color-/]",
message:
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
},
],
},
},
{
files: ["frontend/src/metabase/**/*"],
rules: {
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/mb-base-color-/]",
message:
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
},
{
selector:
"CallExpression[callee.property.name='legacyQuery'] > ObjectExpression > Property[key.name='useStructuredQuery'][value.value=true]",
message: "StructuredQuery usage is forbidden. Use MLv2",
},
],
},
},
],
};

6
.github/CODEOWNERS vendored
View File

@@ -1,12 +1,14 @@
# 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

@@ -66,13 +66,12 @@ body:
Copy and paste the JSON from `Admin -> Troubleshooting`.
_This will be automatically formatted into code, so no need for backticks._
placeholder: |
{"browser-info": {
"language": "en-US",
"platform": "MacIntel",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"vendor": "Google Inc."},
"metabase-info": {
"databases": [ "postgres",...
- Your browser and the version: (e.g., Chrome 52.1, Firefox 48.0, Safari 11.1, …)
- Your operating system: (e.g., OS X 10.10, Windows 10.1809, Ubuntu 16.04, …)
- Your databases: (e.g, MySQL, Postgres, MongoDB, …)
- Metabase version: (e.g., 0.19.3)
- Metabase hosting environment: (e.g., Elastic Beanstalk, Docker, Heroku, Jar-file on Ubuntu, …)
- Metabase internal database: (e.g., H2 (default), Postgres or MySQL)
render: JSON
validations:
required: true

View File

@@ -5,19 +5,20 @@ about: This issue is used to track a feature/project that may span several days
and manage work.
title: "[Epic] Title"
labels: ".Epic"
---
### Links
**Links**
- product doc: _link to product doc_
- eng doc: _link to technical design doc, if any_
- feature branch: `branch-name` _this should be the feature branch where this work will be done in. PRs will be delivered against this branch_
- issue links: _related issues if any_
- [product doc](LINK_TO_PRODUCT_DOC)
- [technical design doc](LINK_TO_TECHNICAL_DESIGN_DOC)
- [testing plan](LINK_TO_TESTING_PLAN)
- feature branch: `BRANCH_NAME`
- issue links: _related issues, if any_
**Implementation Plan**
### Implementation Plan
```[tasklist]
#### Milestone 1
- [ ] _Add tasks here_
```
***Milestone 1***
_insert tasklist here_
***Milestone 2***

View File

@@ -1,5 +1,9 @@
> [!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.
> 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.
> **Warning**
>

View File

@@ -1,15 +0,0 @@
name: Wait for localhost port to be ready
inputs:
port:
required: true
runs:
using: composite
steps:
- name: Wait for port ${{ inputs.port }} to be ready
shell: bash
run: |
while ! nc -z localhost ${{ inputs.port }}; do
sleep 0.1;
done

View File

@@ -13,9 +13,10 @@ runs:
id: matrix
with:
script: | # js
const java = 21;
const java = 11;
const defaultRunner = "ubuntu-22.04";
const beefierRunner = "buildjet-2vcpu-ubuntu-2204";
const defaultOptions = {
"java-version": java,
@@ -34,13 +35,9 @@ runs:
["dashboard", {} ],
["dashboard-cards", {} ],
["dashboard-filters", {} ],
["dashboard-filters-2", {} ],
["embedding", {} ],
["embedding-sdk", { context: "embedding-sdk" } ],
["filters", {} ],
["filters-reproductions", {}],
["joins", {} ],
["metrics", {} ],
["models", {} ],
["native", {} ],
["native-filters", {} ],
@@ -48,8 +45,6 @@ runs:
["organization", {} ],
["permissions", {} ],
["question", {} ],
["question-reproductions", {} ],
["search", {}],
["sharing", {} ],
["visualizations-charts", {} ],
["visualizations-tabular", {} ],

View File

@@ -1,132 +0,0 @@
name: Build ee-extra
description: Build Metabase Enterprise Edition with extra features
inputs:
java-version:
required: true
# build with Java 11 since that's still officially what we target as a minimum and we don't want Java >11 bytecode
# sneaking in.
default: 11
iam-role:
description: "The IAM role to assume"
required: true
runs:
using: composite
steps:
- name: Prepare Java
uses: actions/setup-java@v3
with:
java-version: ${{ inputs.java-version }}
distribution: 'temurin'
- name: Reveal its original version.properties
run: jar xf ./bin/docker/metabase.jar version.properties && cat version.properties
shell: bash
- name: get major version
id: major_version
run: echo "major_version=$(cat version.properties | grep 'tag=' | cut -d'.' -f2)" >> $GITHUB_OUTPUT
shell: bash
- name: get drivers registry
run: |
curl -OL https://raw.githubusercontent.com/metabase/metabase-registry/main/registry.yaml
shell: bash
- uses: actions/setup-node@v4
with:
node-version: lts/Hydrogen # 18.x.x
- run: rm package.json && npm install yaml
shell: bash
- name: parse drivers registry
uses: actions/github-script@v7
id: drivers_registry
with:
script: | # js
const { parse } = require('yaml');
const fs = require('fs');
const registry = parse(fs.readFileSync('registry.yaml', 'utf8'));
const drivers = registry.drivers;
const majorVersion = '${{ steps.major_version.outputs.major_version }}';
const getDriverUrl = (driverName) => {
const driver = drivers.find(driver => driver.name === driverName);
if (!driver) {
throw new Error(`Driver ${driverName} not found in the registry`);
}
const driverUrl = driver.versions[majorVersion] ?? driver.versions.default;
if (!driverUrl) {
throw new Error(`Driver ${driverName} version ${majorVersion} not found in the registry`);
}
return driverUrl;
};
const extraDrivers = {
firebolt: getDriverUrl('firebolt'),
starburst: getDriverUrl('starburst'),
exasol: getDriverUrl('exasol'),
exasol_jdbc: drivers.find(d => d.name === 'exasol').deps[0],
clickhouse: getDriverUrl('clickhouse'),
ocient: getDriverUrl('ocient'),
materialize: getDriverUrl('materialize'),
};
console.log(extraDrivers);
return extraDrivers;
- name: Download Firebolt driver
run: |
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).firebolt }}
working-directory: modules
shell: bash
- name: Download Starburst driver
run: |
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).starburst }}
shell: bash
working-directory: modules
- name: Download Exasol driver
run: |
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).exasol }}
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).exasol_jdbc }}
shell: bash
working-directory: modules
- name: Download Clickhouse driver
run: |
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).clickhouse }}
shell: bash
working-directory: modules
- name: Download Ocient driver
run: |
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).ocient }}
shell: bash
working-directory: modules
- name: Download Materialize driver
run: |
curl -OL ${{ fromJson(steps.drivers_registry.outputs.result).materialize }}
shell: bash
working-directory: modules
- name: Bundle additional drivers into the Uberjar
run: jar uf bin/docker/metabase.jar modules/*.jar
shell: bash
- name: Launch uberjar
run: >-
java --add-opens java.base/java.nio=ALL-UNNAMED -jar ./bin/docker/metabase.jar &
shell: bash
- name: Wait for Metabase to start
run: while ! curl 'http://localhost:3000/api/health' | grep '{"status":"ok"}'; do sleep 1; done
shell: bash
- name: Kill the java process
run: pkill -f metabase.jar
shell: bash
- name: configure aws credentials
if: ${{ github.ref_name == 'master' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ inputs.iam-role }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: us-east-1
- name: Login to Amazon ECR
if: ${{ github.ref_name == 'master' }}
uses: aws-actions/amazon-ecr-login@v2

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

@@ -10,27 +10,24 @@ inputs:
maildev:
description: Maildev container
required: true
default: false
default: 'false'
openldap:
description: OpenLDAP container
required: true
default: false
default: 'false'
postgres:
description: Postgres container
required: true
default: false
default: 'false'
mysql:
description: MySQL container
required: true
default: false
default: 'false'
mongo:
description: Mongo container
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

@@ -2,7 +2,7 @@ name: Prepare back-end environment
inputs:
java-version:
required: true
default: '21'
default: '11'
clojure-version:
required: true
default: '1.11.1.1262'

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-workflows
'src/metabase/db/custom_migrations.clj':
- team:core-workflows
options:
ignore_draft: true
ignored_keywords:
- DO NOT REVIEW
- WIP
enable_group_assignment: false

View File

@@ -35,22 +35,14 @@ frontend_specs: &frontend_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 +53,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 +71,6 @@ backend_specs: &backend_specs
- "modules/drivers/*/test/**"
- "test_config/**"
- "dev/**"
- "test_resources/**"
backend_all: &backend_all
- *default
@@ -110,10 +100,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;

53
.github/team.json vendored
View File

@@ -1,53 +1,50 @@
{
"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",
"romeovs",
"bshepherdson",
"metamben",
"lbrdnk",
"appleby",
"uladzimirdev"
"nemanjaglumac",
"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", "oisincoveney", "npretto", "heypoom"]
"members": ["WiNloSt", "deniskaber", "oisincoveney", "npretto", "heypoom"]
},
{
"name": "Workflows",
"label": ".Team/Workflows",
"name": "BEC",
"label": ".Team/BackendComponents",
"projectUrl": "https://github.com/orgs/metabase/projects/50/views/1",
"members": [
"tsmacdonald",
"qnkhuat",
"calherries",
"piranha",
"crisptrutski",
"deniskaber"
"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"
]
},
{
@@ -55,16 +52,10 @@
"label": ".Team/DashViz",
"members": [
"alxnddr",
"EmmadUsmani",
"JesseSDevaney",
"kulyk",
"adam-james-v",
"tsplude"
]
},
{
"name": "DevEx",
"label": ".Team/DevEx",
"members": [
"camsaul"
"adam-james-v"
]
}
]

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,29 +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:
# don't run this workflow on a cron for forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ubuntu-22.04
timeout-minutes: 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,29 @@ 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@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./target/coverage/codecov.json
flags: back-end
be-linter-clj-kondo:
needs: [files-changed, static-viz-files-changed]
if: |
@@ -67,12 +93,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
@@ -168,6 +205,12 @@ jobs:
- name: Run tests
id: run-java-tests
if: matrix.java-version != 21
run: clojure -X:dev:ci:test:${{ matrix.edition }}:${{ matrix.edition }}-dev
- name: If using Java 21, only run tests on `master`
id: run-java-tests-21
if: matrix.java-version == 21 && github.ref_name == 'master'
run: clojure -X:dev:ci:test:${{ matrix.edition }}:${{ matrix.edition }}-dev
- name: Upload Test Results
@@ -248,18 +291,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

@@ -3,13 +3,14 @@ name: Bugs Slack Notification
on:
issues:
types:
- labeled
- opened
jobs:
on-issue-labeled:
on-issue-opened:
runs-on: ubuntu-22.04
timeout-minutes: 5
if: |
github.event.label.name == '.Needs Triage' && contains(github.event.issue.labels.*.name, 'Type:Bug')
contains(github.event.issue.labels.*.name, '.Needs Triage') &&
contains(github.event.issue.labels.*.name, 'Type:Bug')
steps:
- name: Setting title
uses: actions/github-script@v7
@@ -33,7 +34,7 @@ jobs:
- name: Send the issue ${{ github.event.issue.number }} to Slack
id: slack
if: steps.check_labels.outputs.has_team_label == 'false'
uses: slackapi/slack-github-action@v1.26.0
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{

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 }}';
@@ -243,11 +221,24 @@ jobs:
./COMMIT-ID
./SHA256.sum
run-pre-release-tests:
name: Run pre-release tests for ${{ inputs.version }}
needs: build-uberjar-for-release
uses: ./.github/workflows/pre-release.yml
secrets: inherit
with:
commit: ${{ inputs.commit }}
version: ${{ inputs.version }}
trigger-tests:
needs: [build-uberjar-for-release]
permissions:
actions: write
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- name: trigger pre-release testing
uses: actions/github-script@v7
with:
script: | # js
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'pre-release.yml',
ref: '${{ github.ref }}',
inputs: {
commit: '${{ inputs.commit }}',
version: '${{ inputs.version }}',
}
});

View File

@@ -1,85 +1,80 @@
name: Automatically set milestone
run-name: Set milestone for ${{ github.event.head_commit.message || inputs.version }}
name: Milestone Reminder
on:
push:
pull_request:
types: [closed]
branches:
- release-x.**
workflow_dispatch:
inputs:
version:
description: 'Major version number (e.g. 52, 78, 109)'
type: number
required: true
- "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: 30
env:
isNewReleaseBranch: ${{ github.event_name == 'workflow_dispatch' }}
timeout-minutes: 5
permissions:
pull-requests: write
issues: write
contents: read
issues: read
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
fetch-depth: ${{ env.isNewReleaseBranch == 'false' && 3 || 0 }} # we need the full history to get all the commit messages
ref: master # we want the logic from master, even though we're triggering on a release branch
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- uses: actions/github-script@v7
if: ${{ env.isNewReleaseBranch == 'false' }}
name: Set milestone for single commit
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,
});
- name: Get commit messages for release branch
if: ${{ env.isNewReleaseBranch == 'true' }}
run: |
npm i -g tsx
tsx release/src/release-branch-commits.ts ${{ inputs.version }} > commit-messages.json
- uses: actions/github-script@v7
if: ${{ env.isNewReleaseBranch == 'true' }}
name: Set milestones for new release branch
env:
# this token has a higher rate limit than the default token, and
# we will need it for this workflow to work
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
with:
script: | # js
const { setMilestoneForCommits } = require('${{ github.workspace }}/release/dist/index.cjs');
const commitMessages = require('${{ github.workspace }}/commit-messages.json');
const owner = context.repo.owner;
const repo = context.repo.repo;
if (getMilestone(pull)) {
console.log("Pull request has milestone", pull.data.milestone?.title);
process.exit(0);
}
const versionNumber = context.payload.inputs.version;
const branchName = `release-x.${versionNumber}.x`;
const issueId = getLinkedIssueId(pull);
console.log(`Found ${commitMessages.length} new commits for ${branchName}`);
if (issueId) {
console.log('linked issue found', issueId);
await setMilestoneForCommits({
github,
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,
branchName,
commitMessages: commitMessages,
ignoreExistingMilestones: true,
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,17 +40,13 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-athena-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -83,8 +79,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -118,38 +113,6 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
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-databricks-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'
steps:
- uses: actions/checkout@v4
- name: Test Databricks driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-ee'
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
@@ -184,8 +147,67 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-druid-jdbc-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- 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-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: 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: ":exclude-tags '[:mb/once]'"
- 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-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: '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]"'
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -203,10 +225,6 @@ jobs:
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
partition-index: [0, 1, 2, 3]
env:
CI: 'true'
DRIVERS: mysql
@@ -227,9 +245,6 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-10-2-ee'
test-args: >-
:partition/total 4
:partition/index ${{ matrix.partition-index }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -247,10 +262,6 @@ jobs:
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
partition-index: [0, 1, 2, 3]
env:
CI: 'true'
DRIVERS: mysql
@@ -271,9 +282,6 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-latest-ee'
test-args: >-
:partition/total 4
:partition/index ${{ matrix.partition-index }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -307,9 +315,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-4-4-ee'
test-args: >-
:exclude-tags [:mongo-sharded-cluster-tests]
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -336,19 +342,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Spin up Mongo docker container
run: >-
docker run -d
-p 27017:27017
--name metamongo
metabase/qa-databases:mongo-sample-4.4
mongod
--dbpath /data/db2/
--tlsMode requireTLS
--tlsCertificateKeyFile /etc/mongo/metamongo.pem
--tlsCAFile /etc/mongo/metaca.crt
- uses: ./.github/actions/await-port
with:
port: 27017
run: docker run -d -p 27017:27017 --name metamongo metabase/qa-databases:mongo-sample-4.4 mongod --dbpath /data/db2/ --tlsMode requireTLS --tlsCertificateKeyFile /etc/mongo/metamongo.pem --tlsCAFile /etc/mongo/metaca.crt
- name: Wait until the port 27017 is ready
run: while ! nc -z localhost 27017; do sleep 1; done
timeout-minutes: 5
- name: Make SSL certificates for Mongo available
run: |
@@ -360,13 +356,12 @@ jobs:
curl https://raw.githubusercontent.com/metabase/metabase-qa/master/dbs/mongo/certificates/metaca.crt \
-o ./test_resources/ssl/mongo/metaca.crt
- name: Test MongoDB SSL driver (4.4)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-4-4-ee'
test-args: >-
:exclude-tags [:mongo-sharded-cluster-tests]
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -400,9 +395,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-5-0-ee'
test-args: >-
:exclude-tags [:mongo-sharded-cluster-tests]
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -429,19 +422,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Spin up Mongo docker container
run: >-
docker run -d
-p 27017:27017
--name metamongo
metabase/qa-databases:mongo-sample-5.0
mongod
--dbpath /data/db2/
--tlsMode requireTLS
--tlsCertificateKeyFile /etc/mongo/metamongo.pem
--tlsCAFile /etc/mongo/metaca.crt
- uses: ./.github/actions/await-port
with:
port: 27017
run: docker run -d -p 27017:27017 --name metamongo metabase/qa-databases:mongo-sample-5.0 mongod --dbpath /data/db2/ --tlsMode requireTLS --tlsCertificateKeyFile /etc/mongo/metamongo.pem --tlsCAFile /etc/mongo/metaca.crt
- name: Wait until the port 27017 is ready
run: while ! nc -z localhost 27017; do sleep 1; done
timeout-minutes: 5
- name: Make SSL certificates for Mongo available
run: |
@@ -453,13 +436,12 @@ jobs:
curl https://raw.githubusercontent.com/metabase/metabase-qa/master/dbs/mongo/certificates/metaca.crt \
-o ./test_resources/ssl/mongo/metaca.crt
- name: Test MongoDB SSL driver (5.0)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-5-0-ee'
test-args: >-
:exclude-tags [:mongo-sharded-cluster-tests]
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -496,9 +478,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-latest-ee'
test-args: >-
:exclude-tags [:mongo-sharded-cluster-tests]
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once :mongo-sharded-cluster-tests]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -548,10 +528,6 @@ jobs:
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
partition-index: [0, 1, 2, 3]
env:
CI: 'true'
DRIVERS: mysql
@@ -572,30 +548,12 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-8-0-ee'
test-args: >-
:partition/total 4
:partition/index ${{ matrix.partition-index }}
- 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-mysql-latest-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
strategy:
fail-fast: false
matrix:
partition-index: [0, 1, 2, 3]
env:
CI: 'true'
DRIVERS: mysql
@@ -631,9 +589,6 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-latest-ee'
test-args: >-
:partition/total 4
:partition/index ${{ matrix.partition-index }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -671,8 +626,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-oracle-18-4-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -719,8 +673,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-oracle-21-3-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -738,28 +691,6 @@ jobs:
if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 12.x
junit-name: postgres-ee
docker-image: postgres:12-alpine
env:
enable-ssl-tests: 'false'
- name: Postgres Latest
junit-name: postgres-latest-ee
docker-image: postgres:latest
env:
enable-ssl-tests: 'true'
job:
- name: Driver Tests
test-args: >-
:only-tags [:mb/driver-tests]
- name: App DB Tests
test-args: >-
:exclude-tags [:mb/driver-tests]
name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
env:
CI: 'true'
DRIVERS: postgres
@@ -769,13 +700,9 @@ jobs:
MB_DB_DBNAME: mb_test
MB_DB_USER: mb_test
MB_POSTGRESQL_TEST_USER: mb_test
# SSL tests are only enabled for the postgres-latest job.
MB_POSTGRES_SSL_TEST_SSL: ${{ matrix.version.env.enable-ssl-tests }}
MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: 'test-resources/certificates/us-east-2-bundle.pem'
services:
postgres:
image: ${{ matrix.version.docker-image }}
image: postgres:12-alpine
ports:
- "5432:5432"
env:
@@ -783,24 +710,55 @@ jobs:
POSTGRES_DB: mb_test
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
test-args: >-
${{ matrix.job.test-args }}
- 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 }}
- uses: actions/checkout@v4
- name: Test Postgres driver (12)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-ee'
be-tests-postgres-latest-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: postgres
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
MB_DB_HOST: localhost
MB_DB_DBNAME: circle_test
MB_DB_USER: circle_test
MB_POSTGRESQL_TEST_USER: circle_test
MB_POSTGRES_SSL_TEST_SSL: true
MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: 'test-resources/certificates/us-east-2-bundle.pem'
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
- name: Test Postgres driver (latest)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-latest-ee'
- 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-presto-jdbc-ee:
needs: files-changed
@@ -827,37 +785,30 @@ jobs:
JAVA_TOOL_OPTIONS: "-Xmx2g"
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/await-port
with:
port: ${{ env.MB_PRESTO_JDBC_TEST_PORT }}
timeout-minutes: 5
- name: Wait for port ${{ env.MB_PRESTO_JDBC_TEST_PORT }} to be ready
run: while ! nc -z localhost ${{ env.MB_PRESTO_JDBC_TEST_PORT }}; do sleep 0.1; done
timeout-minutes: 15
- name: Create temp cacerts file based on bundled JDK one
run: cp $JAVA_HOME/lib/security/cacerts /tmp/cacerts-with-presto-ssl.jks
- name: Capture Presto server self signed CA
run: |
while [[ ! -s /tmp/presto-ssl-ca.pem ]]; do
echo "Waiting to capture SSL CA" \
while [[ ! -s /tmp/presto-ssl-ca.pem ]];
do echo "Waiting to capture SSL CA" \
&& openssl s_client -connect localhost:8443 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/presto-ssl-ca.pem \
&& sleep 1;
done
&& sleep 1; done
- name: Convert Presto CA from PEM to DER
run: openssl x509 -outform der -in /tmp/presto-ssl-ca.pem -out /tmp/presto-ssl-ca.der
- name: Add write permission on cacerts file
run: chmod u+w /tmp/cacerts-with-presto-ssl.jks
- name: Import Presto CA into temp cacerts file
run: >-
keytool -noprompt -import
-alias presto
-keystore /tmp/cacerts-with-presto-ssl.jks
-storepass changeit
-file /tmp/presto-ssl-ca.der
-trustcacerts
run: |
keytool -noprompt -import -alias presto -keystore /tmp/cacerts-with-presto-ssl.jks \
-storepass changeit -file /tmp/presto-ssl-ca.der -trustcacerts
- name: Test Presto JDBC driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-presto-jdbc-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -888,8 +839,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-redshift-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -916,19 +866,13 @@ 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
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-snowflake-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -960,8 +904,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sparksql-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -988,8 +931,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlite-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -1005,7 +947,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'
@@ -1028,8 +970,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlserver-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -1068,8 +1009,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlserver-2022-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -1103,8 +1043,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-vertica-ee'
test-args: >-
:only-tags [:mb/driver-tests]
test-args: ":exclude-tags '[:mb/once]'"
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -1116,41 +1055,3 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
# Dummy job generator to spit out fake runs for matrix jobs we skipped. See
# https://github.com/orgs/community/discussions/9141 and
# https://metaboat.slack.com/archives/C0669P4AF9N/p1732315370301059
be-driver-tests-skipped-stub:
needs: files-changed
if: |
!cancelled() &&
(github.event.pull_request.draft == true ||
needs.files-changed.outputs.backend_all == 'false')
strategy:
matrix:
job:
- "Test Postgres 12.x (App DB Tests)"
- "Test Postgres 12.x (Driver Tests)"
- "Test Postgres Latest (App DB Tests)"
- "Test Postgres Latest (Driver Tests)"
- "be-tests-mariadb-10-2-ee (0)"
- "be-tests-mariadb-10-2-ee (1)"
- "be-tests-mariadb-10-2-ee (2)"
- "be-tests-mariadb-10-2-ee (3)"
- "be-tests-mariadb-latest-ee (0)"
- "be-tests-mariadb-latest-ee (1)"
- "be-tests-mariadb-latest-ee (2)"
- "be-tests-mariadb-latest-ee (3)"
- "be-tests-mysql-8-0-ee (0)"
- "be-tests-mysql-8-0-ee (1)"
- "be-tests-mysql-8-0-ee (2)"
- "be-tests-mysql-8-0-ee (3)"
- "be-tests-mysql-latest-ee (0)"
- "be-tests-mysql-latest-ee (1)"
- "be-tests-mysql-latest-ee (2)"
- "be-tests-mysql-latest-ee (3)"
name: ${{ matrix.job }}
runs-on: ubuntu-22.04
steps:
- run: >-
echo 'Skipping job due to conditional filtering'

View File

@@ -1,240 +0,0 @@
name: E2E Cross-version Component Tests for Embedding SDK
on:
push:
branches:
- "release-**"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- "release-**"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_all: ${{ steps.changes.outputs.e2e_all }}
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
# if this is a test on a release branch, we need to check the build requirements
get-build-requirements:
if: |
!cancelled()
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 targetBranchName = '${{ github.base_ref || github.ref }}';
const version = getVersionFromReleaseBranch(targetBranchName);
const requirements = getBuildRequirements(version);
return {
java_version: requirements.java,
node_version: requirements.node,
};
build:
needs: [files-changed, get-build-requirements]
if: |
!cancelled() &&
github.event.pull_request.draft == false
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
MB_EDITION: ee
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 }}"
- name: Build uberjar with ./bin/build.sh
run: ./bin/build.sh
- name: Prepare uberjar artifact
uses: ./.github/actions/prepare-uberjar-artifact
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
resolve-sdk-version:
if: |
!cancelled()
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:
sdk_version: ${{ fromJson(steps.dependencies.outputs.result).sdk_version }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get latest SDK release for this branch
uses: actions/github-script@v7
id: dependencies
with:
script: | # js
const { getSdkVersionFromReleaseBranchName } = require('${{ github.workspace }}/release/dist/index.cjs');
const branchName = '${{ github.base_ref || github.ref }}';
const sdk_version = await getSdkVersionFromReleaseBranchName({
github,
owner: context.repo.owner,
repo: context.repo.repo,
branchName,
});
return {
sdk_version,
};
e2e-tests:
needs: [get-build-requirements, build, resolve-sdk-version]
if: |
!cancelled() && needs.build.result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-tests-embedding-sdk
env:
MB_EDITION: ee
DISPLAY: ""
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
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.event.head.sha }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
steps:
- uses: actions/checkout@v4
- name: Prepare Docker containers
uses: ./.github/actions/e2e-prepare-containers
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Needed to omit newly added tests for functionality that is not yet released
- name: Checkout SDK release commit
uses: actions/checkout@v4
with:
ref: embedding-sdk-${{ needs.resolve-sdk-version.outputs.sdk_version }}
- name: Retrieve uberjar artifact for ee
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 ${{ needs.get-build-requirements.outputs.java_version || 11 }}
uses: actions/setup-java@v4
with:
java-version: ${{ needs.get-build-requirements.outputs.java_version || 11 }}
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Install Embedding SDK package v${{ needs.resolve-sdk-version.outputs.sdk_version }}
run: yarn add @metabase/embedding-sdk-react@${{ needs.resolve-sdk-version.outputs.sdk_version }}
- name: Run E2E tests for Embedding SDK
run: yarn run test-cypress-run-component-sdk --browser ${{ steps.cypress-prep.outputs.chrome-path }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
output-name: e2e-embedding-sdk
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
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 }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-recording-embedding-sdk-cross-version
path: |
./cypress
./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]
if: |
!cancelled() &&
needs.e2e-tests.result == 'skipped'
runs-on: ubuntu-22.04
timeout-minutes: 5
name: e2e-component-tests-embedding-sdk-cross-version
steps:
- run: |
echo "Didn't run due to conditional filtering"

View File

@@ -1,212 +0,0 @@
name: E2E Component Tests for 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:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_all: ${{ steps.changes.outputs.e2e_all }}
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
# if this is a test on a release branch, we need to check the build requirements
get-build-requirements:
if: |
!cancelled() &&
contains(github.base_ref || 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 targetBranchName = '${{ github.base_ref || github.ref }}';
const version = getVersionFromReleaseBranch(targetBranchName);
const requirements = getBuildRequirements(version);
return {
java_version: requirements.java,
node_version: requirements.node,
};
build:
needs: [files-changed, get-build-requirements]
if: |
!cancelled() &&
github.event.pull_request.draft == false &&
needs.files-changed.outputs.e2e_all == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
MB_EDITION: ee
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 }}"
- name: Build uberjar with ./bin/build.sh
run: ./bin/build.sh
- name: Prepare uberjar artifact
uses: ./.github/actions/prepare-uberjar-artifact
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
e2e-tests:
needs: [get-build-requirements, build]
if: |
!cancelled() && needs.build.result == 'success'
runs-on: ubuntu-22.04
timeout-minutes: 45
name: e2e-tests-embedding-sdk
env:
MB_EDITION: ee
DISPLAY: ""
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
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.event.head.sha }}
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
steps:
- uses: actions/checkout@v4
- name: Prepare Docker containers
uses: ./.github/actions/e2e-prepare-containers
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Retrieve uberjar artifact for ee
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 ${{ needs.get-build-requirements.outputs.java_version || 11 }}
uses: actions/setup-java@v4
with:
java-version: ${{ needs.get-build-requirements.outputs.java_version || 11 }}
distribution: "temurin"
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- 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 Embedding SDK package
run: yarn build-embedding-sdk
- name: Run E2E tests for Embedding SDK
run: yarn run test-cypress-run-component-sdk --browser ${{ steps.cypress-prep.outputs.chrome-path }}
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit
output-name: e2e-embedding-sdk
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
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 }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-recording-embedding-sdk-latest
path: |
./cypress
./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]
if: |
!cancelled() &&
needs.e2e-tests.result == 'skipped'
runs-on: ubuntu-22.04
timeout-minutes: 5
name: e2e-component-tests-embedding-sdk
steps:
- run: |
echo "Didn't run due to conditional filtering"

View File

@@ -2,33 +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
# don't run this workflow on a cron for forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ubuntu-22.04
timeout-minutes: 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: [
@@ -73,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 }}
@@ -106,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
@@ -119,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
@@ -132,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');
@@ -147,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,
}
},
@@ -159,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,9 @@ 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_QA_DB_MONGO: 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 +67,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
@@ -87,13 +78,12 @@ jobs:
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare JDK 21
- name: Prepare JDK 11
uses: actions/setup-java@v4
with:
java-version: 21
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 +96,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.event.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,36 +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:
# don't run this workflow on a cron for forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ubuntu-22.04
timeout-minutes: 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,37 +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:
# don't run this workflow on a cron for forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ubuntu-22.04
timeout-minutes: 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

@@ -10,9 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
ref: ${{ github.head_ref }}
- name: Prepare frontend environment
@@ -27,9 +26,7 @@ jobs:
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
run: yarn build-storybook
- name: Serve Storybook
run: yarn http-server storybook-static -p 6006 &
@@ -47,23 +44,29 @@ jobs:
- name: Update Loki Snapshots
run: yarn test-visual:loki-update
- 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 }}
- name: Commit and Push Changes
run: |
git config --global user.name "Metabase Automation"
git config --global user.email "github-automation@metabase.com"
git config --global user.name "Metabase bot"
git config --global user.email "metabase-bot@metabase.com"
git add .loki/**
git commit -m "Update Loki Snapshots"
git push
env:
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
- name: Remove 'loki-update' label
uses: actions/github-script@v7
uses: actions/github-script@v3
with:
github-token: ${{secrets.METABASE_AUTOMATION_USER_TOKEN}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.removeLabel({
github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'loki-update'
});
})

View File

@@ -3,26 +3,7 @@ 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:
@@ -38,7 +19,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Prepare frontend environment
uses: ./.github/actions/prepare-frontend
@@ -52,9 +33,7 @@ jobs:
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
run: yarn build-storybook
- name: Serve Storybook
run: yarn http-server storybook-static -p 6006 &
@@ -71,7 +50,7 @@ jobs:
- name: Run Loki Visual Tests
run: |
yarn test-visual:loki
yarn loki test
- name: Generate Visual Report on Failure
if: failure()
@@ -83,6 +62,5 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: loki-report
include-hidden-files: true
path: .loki/
if-no-files-found: ignore

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

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