Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
44d9a5f
🤖 backported "Skip API-key users when expanding notification recipien…
github-automation-metabase May 19, 2026
ab3aae9
🤖 backported "docs: usage analytics updates" (#74424)
github-automation-metabase May 19, 2026
8ef336a
🤖 backported "Return client error when unable to acquire DB connectio…
github-automation-metabase May 19, 2026
ec7263e
🤖 backported "docs: ai updates" (#74426)
github-automation-metabase May 19, 2026
401a457
🤖 backported "Defensively support double-encoded search index rows" (…
github-automation-metabase May 19, 2026
868cdf9
🤖 backported "Update Security Center promo card layout" (#74421)
github-automation-metabase May 19, 2026
f2b13bb
🤖 backported "Add CSP nonce to SAML login popup for embedding" (#74212)
github-automation-metabase May 19, 2026
e581250
🤖 backported "docs: remove exp recommendation" (#74431)
github-automation-metabase May 19, 2026
eb01f70
🤖 backported "Metabot: read_resource navigation primitives" (#74430)
github-automation-metabase May 19, 2026
3ae638d
🤖 backported "fix(sdk-bundle): host style not respected when navigati…
github-automation-metabase May 19, 2026
152aa09
🤖 backported "Fix impersonated model caching " (#74451)
github-automation-metabase May 19, 2026
5a2d5d8
🤖 backported "Skip H2 databases during serdes extract (GHY-3633)" (#7…
github-automation-metabase May 19, 2026
5bce448
🤖 backported "security-center prometheus metrics" (#74418)
github-automation-metabase May 20, 2026
63b11e9
🤖 backported "Allow serialization to import transform tables after sy…
github-automation-metabase May 20, 2026
5698cb1
🤖 backported "Fix conditional formatting colors for BigDecimal cells …
github-automation-metabase May 20, 2026
0f3f5aa
🤖 backported "Add transform as a serdes dependency of table" (#74481)
github-automation-metabase May 20, 2026
86382f0
🤖 backported "docs: transform addons" (#74504)
github-automation-metabase May 20, 2026
43b5776
🤖 backported "Add no writable databases state to transforms pages" (#…
github-automation-metabase May 20, 2026
0d6c5e3
🤖 backported "docs: add docs for scatterplots and bubble chart" (#74513)
github-automation-metabase May 20, 2026
d973005
🤖 backported "GRO-411 Corrects transform inspector upsell" (#74510)
github-automation-metabase May 20, 2026
4984e19
🤖 backported "Guard v60 dependency_status data migrations against mis…
github-automation-metabase May 20, 2026
672d7b8
🤖 backported "docs: fix minor typo in SDK Bundle description" (#74548)
github-automation-metabase May 20, 2026
6105167
🤖 backported "[AI Analytics] Date filters respect retention period" (…
github-automation-metabase May 20, 2026
0400dc8
🤖 backported "Strip redundant card fields from serdes output" (#74372)
github-automation-metabase May 20, 2026
97472e4
🤖 backported "[AI Analytics] Make conversation list sortable by more …
github-automation-metabase May 20, 2026
eef1dd3
🤖 backported "Reproduce and fix deadlock issue with remote sync" (#74…
github-automation-metabase May 20, 2026
8f5d070
🤖 backported "deps: exclude azure-core-http-netty from snowflake driv…
github-automation-metabase May 21, 2026
ea6a2c3
🤖 backported "fix: add metabase_version to serdes extract" (#74579)
github-automation-metabase May 21, 2026
ce4c838
🤖 backported "Uxw 3359 visualizer breaks internal dimension remapping…
github-automation-metabase May 21, 2026
b8bfba7
🤖 backported "Remote sync upsell" (#74167)
github-automation-metabase May 21, 2026
71ba9ae
🤖 backported "Data Complexity Score rating bands" (#74470)
github-automation-metabase May 21, 2026
84c15b7
🤖 backported "docs(collections): add updates for missing or undocumen…
github-automation-metabase May 21, 2026
a7a3148
🤖 backported "Guard v60 dependency_status drop changesets against mis…
github-automation-metabase May 21, 2026
a928ae3
🤖 backported "Fix sandbox removed field references" (#74616)
github-automation-metabase May 21, 2026
d7e9a78
🤖 backported "Pass label_field for parameter card dropdown sources" (…
github-automation-metabase May 22, 2026
a4e6228
🤖 backported "Fix excessive padding in embed wizard" (#74506)
github-automation-metabase May 22, 2026
2404067
🤖 backported "Fix /api/frontend-errors 404 in embedded contexts" (#74…
github-automation-metabase May 22, 2026
bb21882
🤖 backported "Prepare the MCP for ChatGPT app" (#74603)
github-automation-metabase May 22, 2026
ec78891
🤖 backported "Use transform timeout for queries inside transforms" (#…
github-automation-metabase May 22, 2026
347d0e3
🤖 backported "[perf] Optimize fingerprinters/mode-stats" (#74641)
github-automation-metabase May 22, 2026
2e6679c
🤖 backported "[Metabot] Fix AI settings inputs flickering + losing fo…
github-automation-metabase May 22, 2026
e9d6863
🤖 backported "Allow custom source for ID params" (#74655)
github-automation-metabase May 22, 2026
b38c7d6
🤖 backported "BOT-1495 Trim all metabot tables to MB_AI_USAGE_MAX_RET…
github-automation-metabase May 22, 2026
83c6606
🤖 backported "Enable SMTP debug logs with mail.debug JVM property" (#…
github-automation-metabase May 22, 2026
3fdb3b5
Enable cljfmt comment indentation [61] (#74688)
camsaul May 22, 2026
56ad9a1
🤖 backported "Pin `jest-canvas-mock`" (#74576)
github-automation-metabase May 25, 2026
5d102e7
🤖 backported "Fix Snowflake python transform e2e test: explicit targe…
github-automation-metabase May 25, 2026
51fc3f3
🤖 backported "Handle query cache update race" (#74647)
github-automation-metabase May 25, 2026
23fcfa5
🤖 backported "handle duplicate copies of gson on classpath" (#74696)
github-automation-metabase May 25, 2026
46ddf6d
🤖 backported "Fix remapping with custom values" (#74722)
github-automation-metabase May 25, 2026
be3af16
🤖 backported "security center: only include mb email if send to all e…
github-automation-metabase May 26, 2026
cd95af1
🤖 backported "Show collection picker when saving new question in meta…
github-automation-metabase May 26, 2026
4b655c9
🤖 backported "feat: ui to display data complexity color coding" (#74761)
github-automation-metabase May 26, 2026
c2ccfdb
🤖 backported "Security center: send test notifications to unsaved rec…
github-automation-metabase May 26, 2026
8b77de7
🤖 backported "Fix search values remap" (#74771)
github-automation-metabase May 26, 2026
e92d71f
🤖 backported "Allow Admins to purchase Metabot AI" (#74786)
github-automation-metabase May 26, 2026
280f65c
🤖 backported "docs - bitbucket remote sync" (#74792)
github-automation-metabase May 26, 2026
c809dec
🤖 backported "MCP: signal tool-list changes to clients (GHY-3661)" (#…
github-automation-metabase May 26, 2026
522257e
🤖 backported "Stop dropping old datasets from snowflake." (#74672)
github-automation-metabase May 26, 2026
fb0577a
🤖 backported "[Metabot] Show message feedback UI in OSS" (#74593)
github-automation-metabase May 26, 2026
888fd6a
🤖 backported "Surface upstream body in LLM provider error logs and me…
github-automation-metabase May 26, 2026
5e06fbc
Enable cljfmt `remove-blank-lines-in-forms?` [61] (#74803)
camsaul May 26, 2026
8c2c8fe
🤖 backported "Fix NullPointerException in render-minibar with missing…
github-automation-metabase May 27, 2026
6b51fd5
Sync to upstream v0.61.3 (conflicts — resolve before merging)
github-actions[bot] Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
metabase.api.macros.defendpoint.tools-manifest/assert-optional-fields-nullable!
metabase.channel.core/send!
metabase.warehouses.models.database/assert-not-h2!
metabase.driver.sql-jdbc.execute/execute-prepared-statement!
Expand Down
6 changes: 4 additions & 2 deletions .clj-kondo/config/modules/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@
:api #{metabase.app-db.cluster-lock ; TODO FIXME
metabase.app-db.core
metabase.app-db.init
metabase.app-db.setup}
metabase.app-db.setup
metabase.app-db.transient-error}
:uses #{auth-provider
classloader
config
Expand Down Expand Up @@ -1679,7 +1680,6 @@
:model/NativeQuerySnippet
:model/Notification
:model/Revision
:model/Table
:model/User}}

queries-rest
Expand Down Expand Up @@ -1928,6 +1928,7 @@
{:team "UX West"
:api #{metabase.server.core
metabase.server.init
metabase.server.middleware.security
metabase.server.middleware.session
metabase.server.settings
metabase.server.streaming-response} ; TODO -- too many API namespaces
Expand Down Expand Up @@ -3241,6 +3242,7 @@
:api #{metabase-enterprise.security-center.api
metabase-enterprise.security-center.init}
:uses #{analytics
analytics-interface
api
app-db
channel
Expand Down
4 changes: 2 additions & 2 deletions .clj-kondo/src/hooks/common.clj
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,8 @@
(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.
;; 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.
Expand Down
4 changes: 2 additions & 2 deletions .clj-kondo/src/hooks/metabase/test/util.clj
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
(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.
;; 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))))
Expand Down
18 changes: 9 additions & 9 deletions .clj-kondo/src/hooks/metabase/util/i18n.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@
(let [c (first chars)]
(if (= c \')
(cond
;; Doubled quotes: skip both
;; Doubled quotes: skip both
(and (next chars) (= (second chars) \'))
(recur (nthrest chars 2))

;; Placeholder escape. We allow for intentional escaping, like either '{0}` or '{{`, but not 'arbitrary
;; strings' So basically, just require the *next* character to be a `{`, find the next single-quote, and
;; continue on from there.
;;
;; Technically a string like "foo bar's happy but i'm not" is totally fine, but we'll say it's invalid
;; because... that inner string literal is probably not what we actually want. So since we've never done
;; this intentionally as far as I can tell, let's just remove the possibility to avoid ambiguity.
;; Placeholder escape. We allow for intentional escaping, like either '{0}` or '{{`, but not 'arbitrary
;; strings' So basically, just require the *next* character to be a `{`, find the next single-quote, and
;; continue on from there.
;;
;; Technically a string like "foo bar's happy but i'm not" is totally fine, but we'll say it's invalid
;; because... that inner string literal is probably not what we actually want. So since we've never done
;; this intentionally as far as I can tell, let's just remove the possibility to avoid ambiguity.
(= (second chars) \{)
(let [at-closing (drop-while #(not= \' %) (next chars))]
(if (seq at-closing)
(recur (next at-closing))
false))
;; Unpaired single quote: invalid
;; Unpaired single quote: invalid
:else
false)
(recur (rest chars)))))))
Expand Down
68 changes: 59 additions & 9 deletions .cljfmt.edn
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
;; Cljfmt config. See https://github.com/weavejester/cljfmt#formatting-options
{:sort-ns-references?
true

:function-arguments-indentation
:community

:parallel?
true
{:function-arguments-indentation :community
:indent-line-comments? true
:normalize-newlines-at-file-end? true
:parallel? true
:remove-blank-lines-in-forms? true
:sort-ns-references? true

:paths
["src"
Expand All @@ -15,11 +13,63 @@
"bin/build"
"bin/lint-migrations-file"
"bin/release-list"
#_"mage/src"
#_"mage/test"
"modules/drivers"
".clj-kondo/src"
".clj-kondo/test"
#_"dev"]

;; a map of symbols that tell cljfmt which forms are allowed to have blank lines inside of them. The value may be
;; either `:all`, which means blank lines are allowed between all elements in the form, e.g. `{cond :all}` to allow
;; blank lines between any of the elements inside cond; or it may be a set of element indexes that are allowed to
;; have blank lines, e.g. `{let #{0}}`, to allow blank lines in the binding of a `let` form.
:extra-blank-line-forms
{are :all
case :all
cond-> :all
cond->> :all
condp :all
clojure.spec.alpha/or :all
clojure.test/are :all
metabase.driver-api.core/match-many :all
metabase.driver-api.core/match-one :all
metabase.driver-api.core/replace :all
metabase.driver-api.core/replace-in :all
metabase.lib.drill-thru.test-util/test-drill-variants-with-merged-args :all
metabase.lib.drill-thru.test-util/test-returns-drill :all
metabase.test/with-temp #{0}
metabase.util.match/match-many :all
metabase.util.match/match-one :all
metabase.util.match/replace :all
metabase.util.match/replace-in :all
;; TODO (Cam 2026-05-21) I'm not really sure about `defprotocol`, `deftype`, `defrecord`, `proxy`, `reify`, etc., I
;; think idiomatically these should probably not have blank lines but we can worry about that later
defprotocol :all
defrecord :all
deftype :all
extend :all
extend-protocol :all
potemkin.types/def-map-type :all
potemkin.types/defprotocol+ :all
potemkin.types/defrecord+ :all
potemkin.types/deftype+ :all
potemkin/def-map-type :all
potemkin/defprotocol+ :all
potemkin/defrecord+ :all
potemkin/deftype+ :all
proxy :all
reify :all
;; TODO (Cam 2026-05-21) -- save these for a follow-on PR
defmacro :all
defmethod :all
defn :all
defn- :all
fn :all
metabase.util.malli/defmethod :all
metabase.util.malli/defn :all
metabase.util.malli/defn- :all}

;; See https://github.com/weavejester/cljfmt/blob/master/cljfmt/resources/cljfmt/indents/clojure.clj for the default
;; definitions
;;
Expand All @@ -37,7 +87,7 @@
;; `: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
{ ;; clojure.core stuff
fn* [[:inner 0]]
let* [[:block 1]]
with-meta [[:default]]
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/drivers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1625,7 +1625,9 @@ jobs:
# if you add a driver job, you must add it to this list for it to be a required check
- be-tests-h2
- be-tests-athena-ee
- be-tests-bigquery-cloud-sdk-ee
# these are not reliable, but we want to run them anyway to gather data about failures
# - be-tests-snowflake-ee
# - be-tests-bigquery-cloud-sdk-ee
- be-tests-druid-ee
- be-tests-databricks-ee
- be-tests-druid-jdbc-ee
Expand All @@ -1637,7 +1639,6 @@ jobs:
- be-tests-postgres
- be-tests-presto-jdbc-ee
- be-tests-redshift-ee
- be-tests-snowflake-ee
- be-tests-sparksql-ee
- be-tests-sqlite-ee
- be-tests-sqlserver
Expand Down Expand Up @@ -1665,7 +1666,7 @@ jobs:
}));

// are all jobs skipped or successful?
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success' || job.name == 'be-tests-starburst-ee'))) {
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {
console.log("");
console.log(" _------. ");
console.log(" / , \_ ");
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .loki/reference/chrome_laptop_Design_System_Colors_Default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion bb.edn
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
;; we put path as bin, and everything is in the ./mage subdirectory,
;; so the namespaces are mage.cli, mage.format, etc.
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
:deps {dev.weavejester/cljfmt {:mvn/version "0.15.3"} ;; run cljfmt from bb
;; use Cam's fork temporarily until a new release with https://github.com/weavejester/cljfmt/pull/420 is cut
;; (release > 0.16.4)
:deps {dev.weavejester/cljfmt {:git/sha "05886be17744846685e6a733973fdfbd24e1acd5"
:git/url "https://github.com/camsaul/cljfmt"} ;; run cljfmt from bb
io.github.paintparty/bling {:mvn/version "0.8.8"} ;; printing bells and whistles
medley/medley {:mvn/version "1.3.0"} ;; utilities
metosin/malli {:mvn/version "0.17.0"} ;; data validation
Expand Down
1 change: 0 additions & 1 deletion bin/build/src/build.clj
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
(run! (comp (partial u/error "Missing License: %s") first)
without-license))
(u/announce "License information generated at %s" output-filename)))

(u/step "Run `bun run generate-license-disclaimer`"
(u/sh {:dir u/project-root-directory}
"bun" "run" "generate-license-disclaimer"))))
Expand Down
81 changes: 77 additions & 4 deletions bin/build/src/build/uberjar.clj
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
(ns build.uberjar
(:require
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.build.api :as b]
[clojure.tools.build.util.zip :as build.zip]
[clojure.tools.namespace.dependency :as ns.deps]
[clojure.tools.namespace.find :as ns.find]
[clojure.tools.namespace.parse :as ns.parse]
[metabuild-common.core :as u]
[metabuild-common.misc :as misc]
[org.corfield.log4j2-conflict-handler :refer [log4j2-conflict-handler]])
(:import
(java.io File OutputStream)
Expand Down Expand Up @@ -167,13 +169,45 @@
#"META-INF/license.*"
#"META-INF/LICENSE.*"])

(defn- prefer-lib
"Returns a conflict handler fn that ensures `preferred` lib's classes always win.
The returned fn writes when the incoming class is from `preferred`, skips otherwise."
[preferred]
(fn prefer-lib' [{:keys [lib path in]}]
(when (= lib preferred)
{:write {path {:stream in}}})))

;; hive-jdbc bundles javax.activation classes with package-private visibility on LogSupport.
;; When these overwrite jakarta.activation's public versions, javax.mail (postal) fails with
;; IllegalAccessError.
(def ^:private activation-conflict-handler
(let [from-com-sun-activation (prefer-lib 'com.sun.activation/jakarta.activation)]
{"com/sun/activation/.*" from-com-sun-activation
"javax/activation/.*" from-com-sun-activation}))

;; vertica-jdbc bundles unshaded gson 2.8.9. When these overwrite the pinned 2.12.1,
;; BigQuery crashes with NoSuchMethodError on JsonWriter.value(float). See #73736.
(def ^:private gson-conflict-handler
{"com/google/gson/.*" (prefer-lib 'com.google.code.gson/gson)})

;; avatica (Hive transitive dep) bundles the entire SLF4J API unshaded.
(def ^:private slf4j-conflict-handler
{"org/slf4j/.*" (prefer-lib 'org.slf4j/slf4j-api)})

(def conflict-handlers
"Merged conflict handlers for the uberjar build. Handles Log4j2 plugin merging,
jakarta.activation class visibility, gson version pinning, and SLF4J API."
(merge log4j2-conflict-handler
activation-conflict-handler
gson-conflict-handler
slf4j-conflict-handler))

(defn- create-uberjar! [basis]
(u/step "Create uberjar"
(with-duration-ms [duration-ms]
(b/uber {:class-dir class-dir
:uber-file uberjar-filename
;; merge Log4j2Plugins.dat files. (#50721)
:conflict-handlers log4j2-conflict-handler
:conflict-handlers conflict-handlers
:basis basis
:exclude dependency-ignore-patterns})
(u/announce "Created uberjar in %.1f seconds." (/ duration-ms 1000.0)))))
Expand Down Expand Up @@ -209,8 +243,8 @@
:when (.isFile f)]
(let [rel-path (.toString (.relativize (.toPath src-dir) (.toPath f)))
target (u/get-path-in-filesystem fs rel-path)]
(Files/createDirectories (.getParent target) (into-array java.nio.file.attribute.FileAttribute []))
(Files/copy (.toPath f) target (into-array java.nio.file.CopyOption [])))))))))
(Files/createDirectories (.getParent target) (misc/varargs java.nio.file.attribute.FileAttribute))
(Files/copy (.toPath f) target (misc/varargs java.nio.file.CopyOption)))))))))

(defn update-manifest!
"Start a build step that updates the manifest.
Expand Down Expand Up @@ -241,3 +275,42 @@
(add-non-aot-driver-sources!)
(update-manifest!))
(u/announce "Built %s in %.1f seconds." uberjar-filename (/ duration-ms 1000.0)))))

(defn detect-class-conflicts
"Run `b/uber` against `basis` (no AOT, no resources) and return a seq of
`{:path ... :lib ...}` for every `.class` file conflict not already handled
by our conflict handlers."
[basis]
(let [conflicts (atom [])]
(clean!)
(b/uber {:class-dir class-dir
:uber-file uberjar-filename
:conflict-handlers (merge conflict-handlers
{:default (fn [{:keys [lib path]}]
(when (str/ends-with? path ".class")
(swap! conflicts conj {:path path :lib lib}))
nil)})
:basis basis
:exclude dependency-ignore-patterns})
@conflicts))

(defn audit-conflicts
"Build a bare uberjar (no AOT, no resources) and report all class file conflicts.
Useful for detecting vendored/unshaded dependencies in fat JARs.

clojure -X:build:build/uberjar build.uberjar/audit-conflicts
clojure -X:build:build/uberjar build.uberjar/audit-conflicts :edition :ee"
[{:keys [edition], :or {edition :ee}}]
(u/step (format "Audit %s uberjar class file conflicts" edition)
(let [basis (create-basis edition)
conflicts (detect-class-conflicts basis)]
(when (seq conflicts)
(u/announce "=== %d class file conflicts detected ===" (count conflicts))
(let [report-file "target/conflict-report.txt"]
(spit report-file
(str/join "\n"
(for [[path libs] (->> conflicts
(group-by :path)
(sort-by key))]
(format "%s — %s" path (str/join ", " (map :lib libs))))))
(u/announce "Conflict report written to %s" report-file))))))
7 changes: 0 additions & 7 deletions bin/build/src/metabuild_common/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@
(p/import-vars
[entrypoint
exit-when-finished-nonzero-on-exception]

[build.env
env-or-throw]

[files
absolute?
assert-file-exists
Expand All @@ -46,28 +44,23 @@
temporary-file
with-open-jar-file-system
zip-directory->file]

[input
interactive?
letter-options-prompt
read-line-with-prompt
yes-or-no-prompt]

[misc
parse-as-keyword
since-ms
start-timer
varargs]

[out
announce
error
pretty-print-exception
safe-println]

[shell
sh
sh*]

[steps
step])
Loading
Loading