* Internal load-namespaces tool to check for compilation errors * Ignore load-namespaces in module checks * lint load-namespaces * Reject positional args * Test the new utility * Guard against nonexistent root. Make the code more testable.
860 lines
51 KiB
Clojure
860 lines
51 KiB
Clojure
{:min-bb-version "1.12.212"
|
|
;; we put path as bin, and everything is in the ./mage subdirectory,
|
|
;; so the namespaces are mage.cli, mage.format, etc.
|
|
;; dev/src is here for tasks whose logic is shared with JVM tests (e.g. dev.kondo-ratchet)
|
|
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src" "dev/src"]
|
|
;; use Git version of dep temporarily until a new release > 0.16.4 is cut
|
|
:deps {dev.weavejester/cljfmt {:git/sha "075eae26e3c8dad433b88f05927bcd50e3571328"
|
|
:git/url "https://github.com/weavejester/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
|
|
mvxcvi/puget {:mvn/version "1.3.4"} ;; pretty printing
|
|
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
|
|
table/table {:mvn/version "0.5.0"} ;; table printing
|
|
}
|
|
;; No :pods key on purpose: tasks lazily load only what they need.
|
|
:tasks
|
|
{:requires [[mage.cli :as cli]
|
|
[mage.color :as c]]
|
|
:init (do (defmacro task! "Binds the parsed command line arguments to `parsed`."
|
|
[& body]
|
|
;; Can't use ` in this file because it is strictly edn!
|
|
(list 'let
|
|
(vector 'parsed (list 'cli/parse! (list 'current-task)))
|
|
(cons 'do body))))
|
|
|
|
alias
|
|
{:doc "Installs 'mage' alias and autocomplete"
|
|
:examples [["./bin/mage alias" "Prints instructions to install the 'mage' alias and autocomplete"]
|
|
["./bin/mage alias install fish" "Installs mage alias for fish shell"]
|
|
["./bin/mage alias print zsh" "Prints shell script to put into .zshrc for zsh users"]]
|
|
:requires [[mage.alias :as alias]]
|
|
:arg-schema [:or
|
|
[:tuple [:enum {:name "shell-type"} "bash" "zsh" "fish"]]
|
|
[:tuple [:enum {:name "mode"} "install" "print"]
|
|
[:enum {:name "shell-type"} "bash" "zsh" "fish"]]]
|
|
:task (task! (alias/instructions parsed))}
|
|
|
|
cljfmt-staged
|
|
{:doc "Runs cljfmt on staged files"
|
|
:requires [[mage.format :as format]]
|
|
:examples [["./bin/mage cljfmt-staged" "Format staged files"]
|
|
["./bin/mage cljfmt-staged -c" "Check staged files"]]
|
|
:options [["-c" "--force-check" "Check staged files"]]
|
|
:task (task! (format/staged parsed))}
|
|
|
|
cljfmt-files
|
|
{:doc "Runs cljfmt on the given files/directories"
|
|
:requires [[mage.format :as format]]
|
|
:examples [["./bin/mage cljfmt-files src/metabase/events.clj" "Format events.clj"]
|
|
["./bin/mage cljfmt-files src" "Format all files in src"]
|
|
["./bin/mage cljfmt-files -c src" "Check all files in src"]]
|
|
:options [["-c" "--force-check" "Check staged files"]]
|
|
:arg-schema [:sequential [:string {:name "file-or-directory"
|
|
:description "Files or directories to format."}]]
|
|
:task (task! (format/files parsed))}
|
|
|
|
cljfmt-all
|
|
{:doc "Runs cljfmt on all (clojure) files"
|
|
:requires [[mage.format :as format]]
|
|
:examples [["./bin/mage cljfmt-all" "Format all files"]]
|
|
:options [["-c" "--force-check" "Check staged files"]]
|
|
:task (task! (format/all parsed))}
|
|
|
|
cljfmt-updated
|
|
{:doc "Runs cljfmt on all (Clojure) files relative to a git ref (default HEAD)"
|
|
:requires [[mage.format :as format]]
|
|
:examples [["./bin/mage cljfmt-updated" "Format updated files relative to HEAD"]
|
|
["./bin/mage cljfmt-updated master" "Format updated files relative to master"]]
|
|
:options [["-c" "--force-check" "Check staged files"]]
|
|
:arg-schema [:or [:tuple] [:tuple [:string {:name "git-ref" :description "Git ref to compare against"}]]]
|
|
:task (task! (format/updated parsed))}
|
|
|
|
kondo
|
|
{:doc "Runs Kondo against a file, directory, or everything we usually lint"
|
|
:examples [["./bin/mage kondo" "run Kondo against everything we usually lint"]
|
|
["./bin/mage kondo src/metabase/db.clj" "run Kondo against a file"]
|
|
["./bin/mage kondo src/metabase/db.clj src/metabase/config.clj" "run Kondo against 2 files"]
|
|
["./bin/mage kondo src/metabase/api/macros" "run Kondo against a directory"]]
|
|
:requires [[mage.kondo :as kondo]]
|
|
:task (task! (kondo/kondo (:arguments parsed)))}
|
|
|
|
kondo-updated
|
|
{:doc "Runs kondo against files changed compared to a git ref"
|
|
:examples [["./bin/mage kondo-updated" "run Kondo on files with changes relative to HEAD"]
|
|
["./bin/mage kondo-updated master" "run Kondo on files with changes relative to master"]]
|
|
:requires [[mage.kondo :as kondo]]
|
|
:task (task! (kondo/kondo-updated (:arguments parsed)))}
|
|
|
|
fix-unused-requires
|
|
{:doc "Removes unused requires flagged by clj-kondo"
|
|
:examples [["./bin/mage fix-unused-requires src/metabase/utils/validate.cljc" "Fix unused requires in a specific file"]
|
|
["./bin/mage fix-unused-requires src" "Fix unused requires in all files in src"]]
|
|
:requires [[mage.fix-unused-requires :as fix]]
|
|
:arg-schema [:sequential [:string {:name "file-or-directory"
|
|
:description "Files or directories to fix unused requires in"}]]
|
|
:task (task! (fix/fix-files parsed))}
|
|
|
|
load-namespaces
|
|
{:doc "Checks that every shipped namespace loads without :dev dependencies (and finds circular dependencies)"
|
|
:examples [["./bin/mage load-namespaces"]]
|
|
:requires [[mage.load-namespaces]]
|
|
:arg-schema [:tuple]
|
|
:task (task! (mage.load-namespaces/load-namespaces))}
|
|
|
|
codespell
|
|
{:doc "Check for misspellings in the source code."
|
|
:examples [["./bin/mage codespell"]]
|
|
:requires [[mage.codespell]]
|
|
:task (task! (mage.codespell/codespell (:arguments parsed)))}
|
|
|
|
codespell-interactive
|
|
{:doc "Check for misspellings in the source code, and interactively prompt to fix them."
|
|
:examples [["./bin/mage codespell-interactive"]]
|
|
:requires [[mage.codespell]]
|
|
:task (task! (mage.codespell/codespell-interactive (:arguments parsed)))}
|
|
|
|
start-maildev
|
|
{:doc "Start Maildev"
|
|
:examples [["./bin/mage start-maildev -h" "print help for start-maildev"]]
|
|
:requires [[mage.start-maildev :as start-maildev]]
|
|
:task (task! (start-maildev/start-maildev!))}
|
|
|
|
tail-d
|
|
{:doc "Tail the #d debug log in real time (see dev.debug). Asks the running REPL for the exact path; falls back to DEV_DEBUG_LOG or /tmp/<dir>_debug.log."
|
|
:examples [["./bin/mage tail-d" "tail the #d log, with bat syntax highlighting if available"]]
|
|
:requires [[mage.tail-d :as tail-d]]
|
|
:task (task! (tail-d/tail-d! parsed))}
|
|
|
|
start-python-runner
|
|
{:doc "Start python-runner container with internal Moto S3 server for Python transforms"
|
|
:examples [["./bin/mage start-python-runner" "start python-runner with internal S3"]]
|
|
:requires [[mage.start-python-runner :as start-python-runner]]
|
|
:task (task! (start-python-runner/start-python-runner!))}
|
|
|
|
start-db
|
|
{:doc "Start a db on a default port in docker"
|
|
:examples [["./bin/mage start-db postgres latest" "start the latest postgres db we support"]
|
|
["./bin/mage start-db mysql oldest" "start the oldest mysql db we support"]
|
|
["./bin/mage start-db postgres latest --port 5555" "start postgres on a custom port"]]
|
|
:requires [[mage.start-db :as start-db]]
|
|
:arg-schema [:tuple
|
|
[:enum :postgres :mysql :mariadb :mongo :clickhouse :sqlserver :oracle]
|
|
[:enum :oldest :latest]]
|
|
:options [["-p" "--port PORT" "Override the default host port for the container" :parse-fn Integer/parseInt]]
|
|
:db-info {:postgres {:ports {:oldest 5432 :latest 5433} :eol-url "https://endoflife.date/api/postgres.json"}
|
|
:mysql {:ports {:oldest 3308 :latest 3309} :eol-url "https://endoflife.date/api/mysql.json"}
|
|
:mariadb {:ports {:oldest 3306 :latest 3307} :eol-url "https://endoflife.date/api/mariadb.json"}
|
|
:mongo {:ports {:oldest 27017 :latest 27018} :eol-url "https://endoflife.date/api/mongodb.json"}
|
|
:sqlserver {:ports {:oldest 1433 :latest 1434} :eol-url "https://endoflife.date/api/mssqlserver.json"}
|
|
:oracle {:ports {:oldest 1521 :latest 1522} :eol-url "https://endoflife.date/api/oracle-database.json"}
|
|
;; some databases aren't on endoflife.date
|
|
:clickhouse {:ports {:oldest 8124 :latest 8123} :eol-url nil}}
|
|
:usage-fn start-db/usage
|
|
:task (task!
|
|
(let [[db version] (:arguments parsed)]
|
|
(start-db/start-db (-> (current-task) :db-info db) db version (:options parsed))))}
|
|
|
|
nrepl
|
|
{:doc "Starts the babashka nrepl: helpful for mage development"
|
|
:requires [[babashka.nrepl.server :as nrepl.server]
|
|
[mage.color :as c]]
|
|
:examples [["./bin/mage nrepl" "Starts the nrepl server"]
|
|
["./bin/mage nrepl --port 50506" "Starts the nrepl server"]
|
|
["./bin/mage nrepl -p 50506" "Starts the nrepl server"]]
|
|
:options [["-p" "--port PORT" "optionally provide a port to run the nrepl server on" :default 1667 :parse-fn Integer/parseInt]]
|
|
:task (task!
|
|
(let [{:keys [port]} (:options parsed)]
|
|
(spit ".nrepl-port" port)
|
|
(nrepl.server/start-server! {:port port})
|
|
(deref (promise))))}
|
|
|
|
lint-migrations
|
|
{:doc "Lint migrations files"
|
|
:requires [[lint-migrations-file]
|
|
[babashka.process :as p]]
|
|
:examples [["./bin/mage lint-migrations" "Lint all migrations and print report"]]
|
|
:task (do (cli/parse! (current-task))
|
|
(p/shell "bin/pre-lint-migrations-file.sh")
|
|
(lint-migrations-file/-main))}
|
|
|
|
new-migration
|
|
{:doc "Create a new directory-based migration file (v60+)"
|
|
:requires [[mage.new-migration :as new-migration]]
|
|
:examples [["./bin/mage new-migration 60 mq_indexes" "Create resources/migrations/060/20260905_mq_indexes.yaml"]]
|
|
:arg-schema [:tuple
|
|
[:int {:name "version" :description "Major version number (e.g., 60)"}]
|
|
[:string {:name "description" :description "Lowercase alphanumeric/underscore name (e.g., mq_indexes)"}]]
|
|
:task (task! (new-migration/new-migration parsed))}
|
|
|
|
jar-download
|
|
{:doc "Download (and optionally run) jar for a metabase version or branch"
|
|
:examples [["./bin/mage jar-download 50" "Download the latest enterprise version of release 50 to ~/path/to/my/metabase/jars"]
|
|
["./bin/mage jar-download 1.45.2 -r" "Download and run metabase_1.45.2.jar"]
|
|
["./bin/mage jar-download 1.45.2 -d ~/path/to/my/jars" "Download metabase_1.45.2.jar to ~/path/to/my/jars, deleting if it exists"]
|
|
["./bin/mage jar-download master --run --port 3001" "Download and run the latest master branch jar on port 3001"]
|
|
["JARS=~/my-stuff ./bin/mage jar-download 1.45.2" "Download metabase_1.45.2.jar to ~/my-stuff"]
|
|
["./bin/mage jar-download v1.56.0-beta -r" "Download and run metabase_1.56.0-beta.jar"]]
|
|
:requires [[mage.jar-download :as jar-download]]
|
|
:options [["-r" "--run" "run the new jar after downloading it"]
|
|
["-e" "--env-file FILE" "you can optionally provide a lein-env file to specify the environment to use when running the jar."]
|
|
["-d" "--delete" "delete the old jar if found, by default does not re-download it"]
|
|
["-p" "--port PORT" "you can optionally provide a port to run the jar on, if not, we use a sane port."]]
|
|
:arg-schema [:or
|
|
[:tuple [:string {:name "version"}]]
|
|
[:tuple [:string {:name "version"}] [:string {:name "path"}]]]
|
|
:usage-fn (fn [_]
|
|
(-> "Version can have the following forms:"
|
|
(str "\n -" (c/green "50") " for the latest enterprise version of release 50")
|
|
(str "\n -" (c/green "1.45.2") " for a specific version")
|
|
(str "\n -" (c/green "master") " for the latest jar created for master")
|
|
(str "\n -" (c/green "any-branch-with-a-pr") " for the latest uberjar created for that branch")
|
|
(str "\n -" (c/green "1.56.0-beta") " for a specific beta version")
|
|
(str "\n\nnote: If you get an error about 'Invalid or corrupt jarfile', run this command again with --delete.")))
|
|
:task (task! (jar-download/jar-download parsed))}
|
|
|
|
setup-stats-repl
|
|
{:doc "Connect to the stats repl"
|
|
:examples [["./bin/mage connect-stats-repl" "Connects to the stats repl"]]
|
|
:requires [[mage.stats-repl :as stats-repl]]
|
|
:task (stats-repl/connect (cli/parse! (current-task)))}
|
|
|
|
notify
|
|
{:doc "Send notification for a comment (that already exists)"
|
|
:requires [[mage.be-dev :refer [nrepl-eval]]]
|
|
:options [["-c" "--comment-id ID" "Notify about this comment id"]
|
|
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
|
:examples [["./bin/mage notify -c 1234" "Notify about comment with id 1234"]
|
|
["./bin/mage notify --comment-id 5678 --port 59498" "Notify about comment with id 5678 on nrepl port 59498"]]
|
|
:task (let [opts (:options (cli/parse! (current-task)))
|
|
;; note: no quoting -> expect it to be int
|
|
code (format "(notify-comment-id! %d)" (parse-long (:comment-id opts)))]
|
|
(if (:port opts)
|
|
(nrepl-eval "metabase.comments.core" code (:port opts))
|
|
(nrepl-eval "metabase.comments.core" code)))}
|
|
|
|
run-tests
|
|
{:doc "Quickly run tests locally. Pass dirs, files, namespaces, or single tests as arguments."
|
|
:examples [["./bin/mage run-tests" "launch interactive test picker"]
|
|
["./bin/mage run-tests test/metabase/db" "run tests in test/metabase/db"]
|
|
["./bin/mage run-tests test/metabase/logger test/metabase/api_keys" "run tests in test/metabase/logger and test/metabase/api_keys"]
|
|
["./bin/mage run-tests test/metabase/config_test.clj" "run all tests in the metabase.config-test namespace"]
|
|
["./bin/mage run-tests metabase.config-test" "run all tests in the metabase.config-test namespace (by name)"]
|
|
["./bin/mage run-tests metabase.config-test/config-test" "run a single test by namespace/test-name"]]
|
|
;; TODO: add option to select files as well as dirs from fzf with styles
|
|
:options [["-s" "--selecting STYLE" "Style of test selection" :default "all"]
|
|
;; all, file, dir
|
|
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
|
:arg-schema [:sequential
|
|
{:details "If none are given, you will be prompted to select directories recursively from test/ and enterprise/backend/test/"}
|
|
[:string {:name "file-dir-ns-or-test" :description "Files, directories (recursive), namespaces, or namespace/test-name to test"}]]
|
|
:requires [[mage.quick-test-runner :as qtr]]
|
|
:task (task! (qtr/go parsed))}
|
|
|
|
affected-modules
|
|
{:doc "Which backend modules have changes?"
|
|
:examples [["./bin/mage affected-modules master" "prints changes compared to `master`"]
|
|
["./bin/mage affected-modules"]]
|
|
:arg-schema [:or [:tuple] [:tuple [:string {:name "git-ref"}]]]
|
|
:requires [[mage.modules]]
|
|
:task (task! (mage.modules/cli-print-affected-modules (:arguments parsed)))}
|
|
|
|
can-skip-driver-tests
|
|
{:doc "Can we skip driver tests for this branch?"
|
|
:examples [["./bin/mage can-skip-driver-tests master" "Returns zero (success) if we can skip driver tests based on changes related to `master`"]
|
|
["./bin/mage can-skip-driver-tests" "Returns zero (success) if we can skip driver tests based on changes? (defaults to master)"]]
|
|
:requires [[mage.modules]]
|
|
:task (task! (mage.modules/cli-can-skip-driver-tests (:arguments parsed)))}
|
|
|
|
fix-modules-config
|
|
{:doc "Regenerate .clj-kondo/config/modules/config.edn so it passes metabase.core.modules-test."
|
|
:examples [["./bin/mage fix-modules-config" "Fix the modules config (uses the running dev REPL if available, else a JVM)"]
|
|
["./bin/mage fix-modules-config --port 59498" "Use the dev REPL on a specific nREPL port"]]
|
|
;; Cold-JVM fallback invokes `clojure -X:dev dev.modules-config/fix-config!`.
|
|
:options [["-p" "--port PORT" "nREPL port to use, defaults to value in .nrepl-port"]]
|
|
:requires [[mage.modules]]
|
|
:task (task! (mage.modules/cli-fix-config (cli/parse! (current-task))))}
|
|
|
|
fix-kondo-ratchets
|
|
{:doc "Lower the per-linter ignore budgets in .clj-kondo/ratchets.edn to match the source tree."
|
|
:examples [["./bin/mage fix-kondo-ratchets" "Ratchet budgets down (never raises one; over-budget linters are only warned about)"]
|
|
["./bin/mage fix-kondo-ratchets --seed :some-linter" "Also set :some-linter's budget to its actual count, for landing a new linter"]]
|
|
:options [["-s" "--seed LINTER" "Set LINTER's budget to the actual count (may add or raise it)"]]
|
|
:requires [[dev.kondo-ratchet]]
|
|
:task (task! (dev.kondo-ratchet/fix! (:options parsed)))}
|
|
|
|
kondo-redundant-ignores
|
|
{:doc "Report inline kondo ignores that are no longer necessary (slow: full kondo run)."
|
|
:examples [["./bin/mage kondo-redundant-ignores" "Scan the backend tree for removable ignores"]
|
|
["./bin/mage kondo-redundant-ignores --fix" "Also remove them from source (restores + marks [kondo-keep] any that re-trigger)"]
|
|
["./bin/mage kondo-redundant-ignores --fix --audit" "Recheck [kondo-keep] sites too, unmarking any now truly redundant"]]
|
|
:options [["-f" "--fix" "Remove the candidate ignores from source"]
|
|
["-a" "--audit" "Include sites marked [kondo-keep] instead of skipping them"]]
|
|
:requires [[mage.kondo-ratchet]]
|
|
:task (task! (mage.kondo-ratchet/redundant-ignores parsed))}
|
|
|
|
kondo-insert-ignores
|
|
{:doc "Insert inline ignores at every site a linter flags -- for landing a new linter without a big-bang fix."
|
|
:examples [["./bin/mage kondo-insert-ignores :metabase/new-linter" "Insert ignores everywhere the linter fires, then seed its budget"]
|
|
["./bin/mage kondo-insert-ignores :equals-true test/metabase/pulse" "Only under the given paths"]]
|
|
:arg-schema [:sequential [:string {:name "linter-then-paths"}]]
|
|
:requires [[mage.kondo-ratchet]]
|
|
:task (task! (mage.kondo-ratchet/insert-ignores (:arguments parsed)))}
|
|
|
|
ls
|
|
{:doc "List all mage public and private tasks"
|
|
:examples [["./bin/mage ls" "List all tasks"]]
|
|
:requires [[mage.util :as u]]
|
|
:task (task! (u/print-tasks parsed))}
|
|
|
|
generate-cursive-resolve-as
|
|
{:doc "Generate XML configuration for Cursive's symbol resolution based on the current clj-kondo lint-as configuration."
|
|
:examples [["./bin/mage generate-cursive-resolve-as" "Generate resolve-as XML configuration"]
|
|
["./bin/mage generate-cursive-resolve-as > /Users/bgrabow/Library/Application\\ Support/JetBrains/IntelliJIdea2025.3/options/ClojureResolveSettings.xml"
|
|
"Generate resolve-as XML configuration and install it in your global editor configuration"]]
|
|
:requires [[mage.cursive-resolve-as :as cursive-resolve-as]]
|
|
:task (task! (cursive-resolve-as/do-it))}
|
|
|
|
analytics:setup
|
|
{:doc "Set up usage analytics development mode"
|
|
:examples [["./bin/mage analytics:setup" "Enable usage analytics dev mode and import content"]]
|
|
:requires [[mage.analytics :as analytics]]
|
|
:task (task! (analytics/setup!))}
|
|
|
|
analytics:export
|
|
{:doc "Export usage analytics content to YAML files"
|
|
:examples [["./bin/mage analytics:export" "Export to default location (resources/instance_analytics)"]
|
|
["./bin/mage analytics:export my-custom-dir" "Export to custom directory"]]
|
|
:arg-schema [:or
|
|
[:tuple]
|
|
[:tuple [:string {:description "Target directory for export"}]]]
|
|
:requires [[mage.analytics :as analytics]]
|
|
:task (task! (if-let [target-dir (first (:arguments parsed))]
|
|
(analytics/export! target-dir)
|
|
(analytics/export!)))}
|
|
|
|
analytics:teardown
|
|
{:doc "Tear down usage analytics development mode"
|
|
:examples [["./bin/mage analytics:teardown" "Remove usage analytics dev database and disable dev mode"]]
|
|
:requires [[mage.analytics :as analytics]]
|
|
:task (task! (analytics/teardown!))}
|
|
|
|
analytics:finalize
|
|
{:doc "Export usage analytics content and tear down development mode"
|
|
:examples [["./bin/mage analytics:finalize" "Export to default location and tear down"]]
|
|
:requires [[mage.analytics :as analytics]]
|
|
:task (task! (do
|
|
(analytics/export!)
|
|
(analytics/teardown!)))}
|
|
|
|
doctor
|
|
{:doc "Check your development environment for issues"
|
|
:examples [["./bin/mage doctor" "Run all health checks"]]
|
|
:requires [[mage.doctor :as doctor]]
|
|
:task (task! (doctor/doctor!))}
|
|
|
|
ci-report
|
|
{:doc "Generate a CI failure report for a PR, branch, or commit"
|
|
:examples [["./bin/mage ci-report" "Check current branch (PR or branch CI)"]
|
|
["./bin/mage ci-report 68292" "Generate report for PR #68292"]
|
|
["./bin/mage ci-report source-replacement" "Report for a branch by name"]
|
|
["./bin/mage ci-report abc123def" "Report for a specific commit SHA"]
|
|
["./bin/mage ci-report --detailed 68292" "Detailed report with full FAIL/ERROR blocks"]
|
|
["./bin/mage ci-report --attempt 1 68292"
|
|
"Report for attempt 1 of PR #68292 (use while a rerun attempt is in flight)"]
|
|
["./bin/mage ci-report --attempt=-1 68292"
|
|
"Report for one attempt prior to the latest (0 = latest, -1 = prior, etc.)"]
|
|
["./bin/mage ci-report --detailed > ci_report.md"
|
|
"Save detailed report to markdown file and print progress."]]
|
|
:options [["-d" "--detailed" "Include full FAIL/ERROR blocks with expected/actual diffs"]
|
|
["-a" "--attempt N" "Workflow run attempt number to fetch (default: latest). Negative N indexes back from latest (0 = latest, -1 = one prior). Useful when a rerun is in progress and you want results from an earlier attempt."
|
|
:parse-fn Integer/parseInt]]
|
|
:usage-fn (fn [_]
|
|
(str "Fetches CI check status and failed job logs from GitHub Actions.\n"
|
|
"Parses Trunk test reports to extract test failure details.\n\n"
|
|
"Accepts a PR number/URL, branch name, or commit SHA.\n"
|
|
"With no arguments, uses the current branch's PR if one exists,\n"
|
|
"otherwise falls back to the current branch's latest CI run.\n\n"
|
|
"Use --detailed to get full test failure output with expected/actual diffs.\n"
|
|
"Use --attempt N to fetch a specific workflow run attempt (e.g. attempt 1\n"
|
|
"while attempt 2 is rerunning). N may be 0 (latest) or negative\n"
|
|
"(e.g. -1 = one attempt prior to the latest).\n\n"
|
|
"Requires GitHub CLI (gh) to be installed and authenticated.\n"
|
|
"If you see 'gh: command not found' or auth errors, run: gh auth login"))
|
|
:requires [[mage.ci-report :as ci-report]]
|
|
:task (task! (ci-report/generate-report! parsed))}
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Private Tasks:
|
|
;; These tasks are more for internal tooling, e.g. for use from git hooks etc.
|
|
;; - hidden from `./bin/mage` listing and `bb tasks`, these all - start with a `-`
|
|
|
|
-driver-decisions
|
|
{:doc "Determine which driver tests should run based on PR context. Outputs decisions for all drivers."
|
|
:examples [["./bin/mage -driver-decisions --git-ref=master --force-run=false --pr-labels= --skip=false" "Determine driver decisions for a PR"]
|
|
["./bin/mage -driver-decisions --pr-labels=ci:run-all-cloud-drivers" "Run all cloud drivers via label"]
|
|
["./bin/mage -driver-decisions --pr-labels=ci:run-mysql-mariadb" "Force-run a specific driver via label"]
|
|
["./bin/mage -driver-decisions --pr-labels=ci:run-all-drivers" "Force-run all drivers via label"]
|
|
["./bin/mage -driver-decisions --github-output-only" "Output only GITHUB_OUTPUT format (for CI)"]]
|
|
:requires [[clojure.string :as str]
|
|
[table.core :as table]
|
|
[mage.modules]]
|
|
:usage-fn
|
|
(fn [_current-task]
|
|
(let [driver-triggers (vec (sort mage.modules/default-modules-which-trigger-drivers))]
|
|
(str
|
|
"# How we decide to run a driver's tests. For each driver, we check these conditions in order (first match wins):\n"
|
|
(with-out-str
|
|
(table/table [["Priority" "Condition" "Result" "Reason"]
|
|
["1" "Global force-run" "RUN" "master/release branch or ci:run-all label (skip list ignored)"]
|
|
["2" "Global skip (no backend changes)" "SKIP" "workflow skip (no backend changes)"]
|
|
["3" "Driver is :h2 or :postgres" "RUN" "H2/Postgres always run"]
|
|
["4" "ci:run-all-drivers or ci:run-DRIVER" "RUN" "ci:run-all-drivers or ci:run-DRIVER label"]
|
|
["5" "Driver's own modules/drivers/* files changed" "RUN" "driver files changed (skip list ignored)"]
|
|
["6" "Configured status = skip" "SKIP" "driver is skipped by CI config"]
|
|
["7" "Cloud driver + ci:run-all-cloud-drivers" "RUN" "ci:run-all-cloud-drivers label"]
|
|
["8" "Cloud driver + query-processor updated" "RUN" "query-processor module updated"]
|
|
["9" "Cloud driver + driver deps affected" "RUN" "driver module affected by shared code changes"]
|
|
["10" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
|
|
["11"
|
|
(str (pr-str driver-triggers) " module deps affected")
|
|
"RUN"
|
|
(str "any of " (pr-str driver-triggers) " affected by module changes")]
|
|
["12" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]]))
|
|
"\nThis table decides only WHETHER a driver runs. Whether its failures GATE is a separate\n"
|
|
"question, answered at the end of the job by ci-conductor's quarantine rules.\n"
|
|
"\nNote: the remote skip list (status = skip) is ignored entirely on a force-run --\n"
|
|
"master and release-* branches, or a ci:run-all label -- so every driver runs there.\n"
|
|
"Whether failures gate remains determined by CI Conductor's quarantine rules. The skip\n"
|
|
"list is also ignored for any driver whose own modules/drivers/* files the run changes.\n"
|
|
"\n\n## Labels of Interest\n\n"
|
|
"These PR labels influence driver test decisions:\n\n"
|
|
" ci:run-all-drivers Force-run ALL drivers (overrides the skip list)\n"
|
|
" ci:run-DRIVER Force-run a specific driver, e.g. ci:run-mysql-mariadb (overrides the skip list)\n"
|
|
" ci:run-all-cloud-drivers Run all cloud drivers (athena, bigquery, databricks, redshift, snowflake)\n"
|
|
"\n## Driver Status (from the CI driver config)\n\n"
|
|
" skip Listed with status \"skip\" in ci-test-config.json: not run at all (changing the\n"
|
|
" driver's own files or ci:run-DRIVER overrides). Every other driver runs and\n"
|
|
" gates as usual.\n"
|
|
"\nTo let a driver run but not gate, add a suite-quarantine rule in CI Conductor instead.\n"
|
|
"\n## All Drivers\n\n"
|
|
(str/join ", " (map name (sort mage.modules/all-drivers)))
|
|
"\n")))
|
|
:options [[nil "--git-ref REF" "The git ref to compute changed modules against"]
|
|
[nil "--force-run FORCE_RUN" "Run every driver regardless of the diff"]
|
|
[nil "--pr-labels PR_LABELS" "Comma delimited labels on the pr"]
|
|
[nil "--skip SKIP"]
|
|
[nil "--github-output-only" "Only output GITHUB_OUTPUT format (no human-readable output)"]]
|
|
|
|
:task (task! (mage.modules/-main parsed))}
|
|
|
|
-install-merge-drivers
|
|
{:doc "Installs custom git merge drivers for YAML migration files."
|
|
:requires [[mage.util :as u]
|
|
[clojure.string :as str]]
|
|
:task (task!
|
|
(let [git-dir (u/sh "git rev-parse --git-common-dir")
|
|
git-config-file (str git-dir "/config")
|
|
the-include (str "\n"
|
|
"[include]\n"
|
|
" path=../.gitconfig\n")]
|
|
(when-not (str/includes? (slurp git-config-file) the-include)
|
|
(println "Merge driver not installed, installing now.")
|
|
(spit git-config-file the-include :append true))))}
|
|
|
|
;; opposite of -install-merge-drivers, removes the include while it's causing issues.
|
|
-uninstall-merge-drivers
|
|
{:doc "Uninstalls custom git merge drivers for YAML migration files."
|
|
:requires [[clojure.string :as str]
|
|
[mage.util :as u]]
|
|
:task (task!
|
|
(let [git-dir (u/sh "git rev-parse --git-common-dir")
|
|
git-config-file (str git-dir "/config")
|
|
the-include (str "\n"
|
|
"[include]\n"
|
|
" path=../.gitconfig\n")]
|
|
(when (str/includes? (slurp git-config-file) the-include)
|
|
(println "Merge driver is installed, uninstalling now.")
|
|
(let [includeless (-> (slurp git-config-file) (str/replace the-include ""))]
|
|
(spit git-config-file includeless)))))}
|
|
|
|
-merge-yaml-migrations
|
|
{:doc "Custom git merge driver for YAML migration files."
|
|
:usage-fn (fn [_]
|
|
(str
|
|
"Treats each changeset as an atomic unit to avoid conflicts within changesets.\n"
|
|
"Preserves original formatting including blank lines and footer warnings"))
|
|
:arg-schema [:tuple
|
|
[:string {:name "base" :description "ancestor's version (base)"}]
|
|
[:string {:name "ours" :description "current version (ours)"}]
|
|
[:string {:name "theirs" :description "other branch's version (theirs)"}]
|
|
[:int {:name "conflict-marker-size" :description "conflict marker size" :min 0}]
|
|
[:string {:name "file-path" :description "file path"}]]
|
|
:requires [[mage.merge-yaml-migrations]]
|
|
:task (task! (mage.merge-yaml-migrations/-main parsed))}
|
|
|
|
-play
|
|
{:doc "Play sounds from mage.sound"
|
|
:examples [["./bin/mage -play success" "play success sound"] ["./bin/mage -play s" "play success sound"]
|
|
["./bin/mage -play error" "play error sound"] ["./bin/mage -play e" "play error sound"]
|
|
["./bin/mage -play ping" "play ping sound"] ["./bin/mage -play p" "play ping sound"]
|
|
["./bin/mage -play warning" "play warning sound"] ["./bin/mage -play w" "play warning sound"]]
|
|
:requires [[mage.sound :as sound]]
|
|
:arg-schema [:tuple [:enum "s" "success" "e" "error" "p" "ping" "w" "warning"]]
|
|
:task (task! (case (first (:arguments parsed))
|
|
("s" "success") (sound/success)
|
|
("e" "error") (sound/error)
|
|
("p" "ping") (sound/ping)
|
|
("w" "warning") (sound/warning)))}
|
|
|
|
-token-scan
|
|
{:doc "Security token scanner for detecting leaked API keys and secrets"
|
|
:examples [["./bin/mage -token-scan deps.edn bb.edn" "Scan specific files"]
|
|
["./bin/mage -token-scan -v deps.edn" "Scan with verbose output"]
|
|
["./bin/mage -token-scan -a" "Scan all files in the project"]]
|
|
:requires [[mage.token-scan :as token-scan]]
|
|
:options [["-a" "--all-files" "Scan all files in the project"]
|
|
["-v" "--verbose" "Verbose output"]]
|
|
:arg-schema [:sequential [:string {:description "Files to scan"}]]
|
|
:task (task! (token-scan/run-scan parsed))}
|
|
|
|
-repl
|
|
{:doc "Eval a string of clojure code in the backend dev server"
|
|
:requires [[babashka.process :as process]
|
|
[mage.color :as c]
|
|
[mage.be-dev :refer [nrepl-eval]]]
|
|
:examples [["./bin/mage -repl '(+ 1 1)'"
|
|
"Eval a form in the backend dev server in the user namespace"]
|
|
["./bin/mage -repl --namespace metabase.app-db.connection @application-db-counter"
|
|
"Eval a form in the backend dev server in the 'metabase.app-db.connection' namespace"]
|
|
["mage -repl --port 59498 --namespace metabase.app-db.connection '(read-string \"::hello\")'"
|
|
"Evaluate a keyword inside a namespace"]]
|
|
:options [["-n" "--namespace NAMESPACE" "Namespace to use for the task"]
|
|
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
|
:task (let [parsed (cli/parse! (current-task))
|
|
code (first (:arguments parsed))
|
|
port (or (:port (:options parsed))
|
|
(slurp ".nrepl-port")
|
|
(throw (ex-info (c/red "No port specified, and no .nrepl-port file found.") {:babashka/exit 1})))
|
|
nns (or (:namespace (:options parsed)) "user")]
|
|
(nrepl-eval nns code port))}
|
|
|
|
-check-readable
|
|
{:doc "Check that code is readable parens a file"
|
|
:requires [[mage.readability-check :as readability]]
|
|
:arg-schema [:or
|
|
[:tuple [:string {:name "file"
|
|
:description "File to check"}]]
|
|
[:tuple
|
|
[:string {:name "file"
|
|
:description "File to check"}]
|
|
[:int {:name "line-number"
|
|
:description "A line in the file" :min 1}]]]
|
|
:examples [["./bin/mage -check-readable dev/src/dev.clj"
|
|
"Check file for readability"]
|
|
["./bin/mage -check-readable dev/src/dev.clj 300"
|
|
"Check top level form containing line 300 for readability"]]
|
|
:task (let [{:keys [arguments]} (cli/parse! (current-task))]
|
|
(apply readability/check arguments))}
|
|
|
|
;; Uncomment this if you want to play with the example calculator task:
|
|
#_#_-example-calculator
|
|
{;; `:doc` is a short docstring for the task, will be listed in `./bin/mage -h` and other help info.
|
|
;; Keep these short, they need to fit on one line in the output from `./bin/mage -h`.
|
|
:doc "The example calculator's task docstring"
|
|
;; examples get printed out when you pass -h or --help.
|
|
:examples [["./bin/mage -example-calculator 1 + 99" "evaluates to 100"]
|
|
["./bin/mage -example-calculator 100 - 99" "evaluates to 1"]]
|
|
;; The task is the actual code that runs when you run the task.
|
|
:task (task!
|
|
(println "Parsed input:") (u/pp parsed)
|
|
(let [{:keys [arguments data]} parsed
|
|
[a op b] arguments]
|
|
(println a (name op) b "=" (c/blue (({:+ + :- -} op) a b)))))
|
|
;; (optional) `:require` lazily libraries for just your task:
|
|
:requires [[mage.color :as c]
|
|
[mage.util :as u]]
|
|
;; (optional) `:options` are passed to [[clojure.tools.cli/parse-opts]].
|
|
;; See: https://clojure.github.io/tools.cli/index.html#clojure.tools.cli/parse-opts
|
|
:options [["-n" "--negate" "Negate the result of the calculation"]
|
|
["-a" "--also-add N" "Add N to the result of the calculation"]]
|
|
;; (optional) `:arg-schema` is a malli schema for the arguments passed to the task, after the options
|
|
:arg-schema [:tuple [:int {:name "number-a"}] [:enum {:name "operation"} :+ :-] [:int {:name "number-b"}]]
|
|
;; (optional) `:usage-fn` is a function called with the current-task map.
|
|
;; Returns a string containing extra-detailed usage information.
|
|
:usage-fn (fn [{:keys [doc] :as _task}]
|
|
(str "Optional extra usage information, for explicit callouts.\n"
|
|
"Reversed docstring: '" (c/green (apply str (reverse doc))) "'"))
|
|
;; (optional) Any other keys are completely allowed. Put things to lookup or that you want to be easy to change
|
|
;; here. These will be returned from `(current-task)`. See: start-db for an example of using `:ports` to define a
|
|
;; top-level map of ports. If something should be easily accessible in the task, put it here.
|
|
:data {:a [:b :c] :b [:d]}}
|
|
|
|
-test {:doc "run all mage tests"
|
|
:requires [[clojure.string :as str]
|
|
[clojure.test]
|
|
[mage.core-test] ;; loads the actual tests
|
|
[mage.util :as u]]
|
|
:arg-schema [:sequential [:string {:name "test-matching-pattern"}]]
|
|
:task (let [{:keys [arguments]} (task! parsed)
|
|
pattern (if (seq arguments)
|
|
(str "^mage\\..*(" (str/join "|" arguments) ").*")
|
|
"^mage\\..*")
|
|
_ (u/debug ["test regex matching pattern" pattern])
|
|
results (clojure.test/run-all-tests (re-pattern pattern))]
|
|
(if (zero? (+ (:fail results) (:error results)))
|
|
(println (c/green "All mage tests passed!"))
|
|
(do
|
|
(println (c/red "Some mage tests failed."))
|
|
(u/exit 1))))}
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Personal Tasks:
|
|
;; These tasks are for personal use, a place for half baked tasks.
|
|
;; Feel free to add your own tasks here, but don't expect them to be run by anyone else, or be listed in the
|
|
;; `./bin/mage` listing.
|
|
|
|
-escherize-pr-summary
|
|
{:doc "Print a day's PR summary for the metabase/metabase repo"
|
|
:requires [[mage.escherize.pr-summary :as eps]]
|
|
:examples [["./bin/mage -escherize-pr-summary | pbcopy" "Copy your daily"]]
|
|
:task (task! (eps/summarize-prs parsed))}
|
|
|
|
-escherize-fix-whitespace
|
|
{:doc "Removes all lines like '^<space>+$' from files in glob pattern."
|
|
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
|
|
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
|
|
:arg-schema [:sequential [:string {:name "glob-pattern" :description "Glob pattern to match files"}]]
|
|
:task (task! (fix-whitespace/run-clean parsed))}
|
|
|
|
-edpaget-dev-env
|
|
{:doc "Stand up (or tear down) an isolated dev environment for this worktree"
|
|
:examples [["./bin/mage -edpaget-dev-env --app-db postgres" "Postgres app-db with auto ports"]
|
|
["./bin/mage -edpaget-dev-env --app-db postgres --with mysql --with mongo" "Full stack"]
|
|
["./bin/mage -edpaget-dev-env" "Minimal: just port config (H2 app-db)"]
|
|
["./bin/mage -edpaget-dev-env --down" "Tear down containers for this worktree"]
|
|
["./bin/mage -edpaget-dev-env --status" "Show running containers and ports"]]
|
|
:requires [[mage.edpaget.dev-env :as dev-env]]
|
|
:options [[nil "--app-db DB" "App database: h2 (default), postgres, mysql" :default "h2"]
|
|
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
|
|
:multi true :default [] :update-fn conj]
|
|
[nil "--down" "Tear down all containers for this worktree"]
|
|
[nil "--status" "Show current env status"]
|
|
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]]
|
|
:task (task! (dev-env/dev-env! parsed))}
|
|
|
|
-johnswanson-dev-env
|
|
{:doc "Stand up (or tear down) an isolated dev environment for this worktree. Interactive prompts when flags omitted."
|
|
:examples [["./bin/mage -johnswanson-dev-env" "Interactive TUI to configure infra (saves config)"]
|
|
["./bin/mage -johnswanson-dev-env --run" "Configure + launch backend/frontend"]
|
|
["./bin/mage -johnswanson-dev-env --run --no-frontend" "Configure + backend only"]
|
|
["./bin/mage -johnswanson-dev-env --down" "Stop everything for this worktree"]
|
|
["./bin/mage -johnswanson-dev-env --status" "Show running containers and processes"]]
|
|
:requires [[mage.johnswanson.dev-env :as jds-dev-env]]
|
|
:options [["-e" "--edition EDITION" "ee or oss (default: prompt)"]
|
|
["-t" "--token TOKEN" "Token type: all-features, starter-cloud, pro-cloud, pro-self-hosted, none"]
|
|
[nil "--app-db DB" "App database: h2, postgres, mysql, mariadb (default: prompt)"]
|
|
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
|
|
:multi true :default [] :update-fn conj]
|
|
[nil "--run" "Also launch backend + frontend in background"]
|
|
[nil "--no-frontend" "Skip frontend dev server (only with --run)"]
|
|
[nil "--down" "Stop everything for this worktree"]
|
|
[nil "--status" "Show current env status"]
|
|
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]
|
|
[nil "--fresh" "Recreate containers from scratch"]]
|
|
:task (task! (jds-dev-env/dev-env! parsed))}
|
|
|
|
-bot-dev-env
|
|
{:doc "Stand up (or tear down) an isolated dev environment for this worktree. Interactive prompts when flags omitted."
|
|
:examples [["./bin/mage -bot-dev-env" "Interactive TUI to configure infra (saves config)"]
|
|
["./bin/mage -bot-dev-env --run" "Configure + launch backend/frontend"]
|
|
["./bin/mage -bot-dev-env --run --no-frontend" "Configure + backend only"]
|
|
["./bin/mage -bot-dev-env --down" "Stop everything for this worktree"]
|
|
["./bin/mage -bot-dev-env --status" "Show running containers and processes"]]
|
|
:requires [[mage.bot.dev-env :as jds-dev-env]]
|
|
:options [["-e" "--edition EDITION" "ee or oss (default: prompt)"]
|
|
["-t" "--token TOKEN" "Token type: all-features, starter-cloud, pro-cloud, pro-self-hosted, none"]
|
|
[nil "--app-db DB" "App database: h2, postgres, mysql, mariadb (default: prompt)"]
|
|
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
|
|
:multi true :default nil :update-fn conj]
|
|
[nil "--bot BOT" "Bot name (enables bot-specific config: Docker containers, bot dirs, Playwright dirs)"]
|
|
[nil "--run" "Also launch backend + frontend in background"]
|
|
[nil "--no-frontend" "Skip frontend dev server (only with --run)"]
|
|
[nil "--down" "Stop everything for this worktree"]
|
|
[nil "--status" "Show current env status"]
|
|
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]
|
|
[nil "--fresh" "Recreate containers from scratch"]
|
|
[nil "--worktree PATH" "Target a different worktree (default: current)"]]
|
|
:task (task! (jds-dev-env/dev-env! parsed))}
|
|
|
|
-bot-setup-worktree
|
|
{:doc "Run final configuration steps after a new worktree is created."
|
|
:examples [["./bin/mage -bot-setup-worktree --worktree $(pwd)" "Set up the current directory as a worktree"]]
|
|
:options [[nil "--worktree PATH" "Path to the worktree to set up (required)"]]
|
|
:requires [[mage.bot.setup-worktree :as setup-worktree]]
|
|
:task (task! (setup-worktree/setup-worktree! parsed))}
|
|
|
|
-bgrabow-ci-test-repl
|
|
{:doc "Start a REPL replicating a CI test job environment (Docker + env vars)"
|
|
:examples [["./bin/mage -bgrabow-ci-test-repl" "Interactive selection with fzf"]
|
|
["./bin/mage -bgrabow-ci-test-repl app-db/be-tests-mysql" "Start REPL for a specific job"]
|
|
["./bin/mage -bgrabow-ci-test-repl -n app-db/be-tests-mysql" "Dry run: print commands without executing"]]
|
|
:requires [[mage.bgrabow.ci-test-repl :as ci-test-repl]]
|
|
:options [["-n" "--dry-run" "Print docker, env, and clojure commands without executing"]]
|
|
:task (task!
|
|
(let [wj (first (:arguments parsed))
|
|
[workflow job] (when wj (clojure.string/split wj (re-pattern "/") 2))]
|
|
(ci-test-repl/ci-test-repl {:workflow workflow :job job :dry-run? (:dry-run (:options parsed))})))}
|
|
|
|
-bot-status
|
|
{:doc "Watch and display bot status"
|
|
:requires [[mage.bot.status :as status]]
|
|
:arg-schema [:? [:string {:name "file-path"}]]
|
|
:task (task! (status/run! parsed))}
|
|
|
|
-bot-generate-prompt
|
|
{:doc "Fill a template with placeholders and write an agent prompt"
|
|
:examples [["./bin/mage -bot-generate-prompt --template dev/bot/fixbot-agent.md --output .bot/fixbot/20260410/prompt.md --set ISSUE_ID=MB-123"
|
|
"Generate fixbot prompt"]]
|
|
:requires [[mage.bot.prompt :as bot-prompt]]
|
|
:options [[nil "--template FILE" "Template file path"]
|
|
[nil "--output FILE" "Output file path"]
|
|
[nil "--set KEY=VALUE" "Set a template variable (can be repeated)"
|
|
:multi true :default [] :update-fn conj]
|
|
[nil "--set-from-file KEY=PATH" "Set a template variable from a file's contents (can be repeated)"
|
|
:multi true :default [] :update-fn conj]]
|
|
:task (task! (bot-prompt/generate-prompt! parsed))}
|
|
|
|
;;; Unified autobot session management
|
|
|
|
-autobot-go
|
|
{:doc "Launch a bot in an autobot session with its own worktree, backend, and frontend (or remote PR env)"
|
|
:examples [["./bin/mage -autobot-go master --bot qabot --command /qabot"
|
|
"Launch qabot in an autobot session"]
|
|
["./bin/mage -autobot-go my-branch --bot fixbot --command '/fixbot MB-12345' --app-db mysql"
|
|
"Launch fixbot with MySQL"]
|
|
["./bin/mage -autobot-go uxw-291 --bot qabot --command /qabot --pr-env-url https://pr383713.coredev.metabase.com"
|
|
"Launch qabot against a remote PR preview environment"]]
|
|
:requires [[mage.bot.autobot :as autobot]]
|
|
:arg-schema [:tuple [:string {:name "branch-name"}]]
|
|
:options [[nil "--bot BOT" "Bot name (qabot, fixbot, uxbot, reprobot, cibot)"]
|
|
[nil "--command CMD" "Inner command to run (e.g., /qabot, '/fixbot MB-12345')"]
|
|
[nil "--app-db DB" "App database" :default "postgres"]
|
|
[nil "--base BASE" "Base branch/commit to branch from. If set and the branch does not exist, it will be created from this base."]
|
|
[nil "--pr-env-url URL" "PR preview environment URL (enables remote mode, no local dev-env)"]]
|
|
:task (task! (autobot/go! parsed))}
|
|
|
|
-bot-pr-env
|
|
{:doc "Configure a worktree for a remote PR preview environment (writes .bot/pr-env.env + caches session token)"
|
|
:examples [["./bin/mage -bot-pr-env --url https://pr383713.coredev.metabase.com --pr 383713"
|
|
"Set up this worktree to talk to the given PR preview environment"]]
|
|
:requires [[mage.bot.pr-env :as pr-env]]
|
|
:options [[nil "--url URL" "PR preview environment URL"]
|
|
[nil "--pr PR" "PR number"]]
|
|
:task (task! (pr-env/setup! parsed))}
|
|
|
|
-autobot-stop
|
|
{:doc "Stop an autobot session (kill tmux + dev env, keep worktree). No args = stop current session."
|
|
:examples [["./bin/mage -autobot-stop qabot-master" "Stop named session"]
|
|
["./bin/mage -autobot-stop" "Stop current session (from inside an autobot session)"]]
|
|
:requires [[mage.bot.autobot :as autobot]]
|
|
:arg-schema [:? [:string {:name "name-or-id"}]]
|
|
:task (task! (autobot/stop! parsed))}
|
|
|
|
-autobot-list
|
|
{:doc "List all autobot sessions with status"
|
|
:examples [["./bin/mage -autobot-list" "List all sessions"]]
|
|
:requires [[mage.bot.autobot :as autobot]]
|
|
:task (task! (autobot/list-all! parsed))}
|
|
|
|
-autobot-result
|
|
{:doc "Print the most recent result.md for a branch+bot combination"
|
|
:examples [["./bin/mage -autobot-result uxw-291-skip-ldap-for-non-ldap qabot"
|
|
"Print qabot's latest result.md for the given branch"]]
|
|
:requires [[mage.bot.autobot :as autobot]]
|
|
:arg-schema [:tuple [:string {:name "branch"}] [:string {:name "bot"}]]
|
|
:task (task! (autobot/result! parsed))}
|
|
|
|
-autobot-kill
|
|
{:doc "Tear down and remove an autobot session. No args = remove current session."
|
|
:examples [["./bin/mage -autobot-kill qabot-master" "Remove named session"]
|
|
["./bin/mage -autobot-kill" "Remove current session (from inside an autobot session)"]]
|
|
:requires [[mage.bot.autobot :as autobot]]
|
|
:arg-schema [:? [:string {:name "name-or-id"}]]
|
|
:task (task! (autobot/kill! parsed))}
|
|
|
|
;;; Shared bot tasks
|
|
|
|
-bot-fetch-issue
|
|
{:doc "Fetch a Linear issue by identifier (shared across bots)"
|
|
:examples [["./bin/mage -bot-fetch-issue MB-12345" "Fetch and display issue"]]
|
|
:requires [[mage.bot.linear :as bot-linear]]
|
|
:arg-schema [:tuple [:string {:name "issue-id"}]]
|
|
:task (task! (bot-linear/print-issue! parsed))}
|
|
|
|
-bot-server-info
|
|
{:doc "Print server configuration (ports, config file, users, API keys) for bot agents"
|
|
:examples [["./bin/mage -bot-server-info" "Show server info"]]
|
|
:requires [[mage.bot.server-info :as server-info]]
|
|
:task (task! (server-info/server-info! parsed))}
|
|
|
|
-bot-preflight
|
|
{:doc "Run prerequisite checks for a bot workflow"
|
|
:examples [["./bin/mage -bot-preflight --bot fixbot" "Run checks for fixbot (Linear required)"]
|
|
["./bin/mage -bot-preflight --bot qabot" "Run checks for qabot (Linear optional)"]]
|
|
:options [[nil "--bot BOT" "Bot name (fixbot, qabot, uxbot, reprobot, cibot)"]]
|
|
:requires [[mage.bot.preflight :as preflight]]
|
|
:task (task! (preflight/preflight! parsed))}
|
|
|
|
-bot-preflight-health
|
|
{:doc "Wait for Metabase backend to become healthy (retries for up to 5 minutes)"
|
|
:examples [["./bin/mage -bot-preflight-health" "Wait for backend health"]]
|
|
:requires [[mage.bot.preflight :as preflight]]
|
|
:task (task! (preflight/preflight-health! parsed))}
|
|
|
|
-bot-timestamp
|
|
{:doc "Print the current wall-clock time as YYYYMMDD-HHMMSS (no other output)"
|
|
:examples [["./bin/mage -bot-timestamp" "Print e.g. 20260413-143022"]]
|
|
:task (task! (println (.format (java.time.LocalDateTime/now)
|
|
(java.time.format.DateTimeFormatter/ofPattern "yyyyMMdd-HHmmss"))))}
|
|
|
|
-bot-repl-eval
|
|
{:doc "Evaluate Clojure code against the best-available REPL (nREPL first, socket REPL fallback)"
|
|
:examples [["./bin/mage -bot-repl-eval '(+ 1 2)'" "Auto-discover and eval"]
|
|
["./bin/mage -bot-repl-eval --type socket '(metabase.config/current-version)'" "Force socket REPL"]
|
|
["./bin/mage -bot-repl-eval --port 50666 '(+ 1 2)'" "Force a specific port"]]
|
|
:requires [[mage.bot.repl-eval :as repl-eval]]
|
|
:arg-schema [:? [:string {:name "code"}]]
|
|
:options [[nil "--code CODE" "Clojure code to eval (alternative to positional arg)"]
|
|
[nil "--port PORT" "Override discovery and use this port"]
|
|
[nil "--host HOST" "Override discovery and use this host (socket REPL only)"]
|
|
[nil "--type TYPE" "Force backend: 'nrepl' or 'socket'. Default: auto-detect."]
|
|
[nil "--refresh" "Ignore .bot/repl.env cache and re-run discovery"]]
|
|
:task (task! (repl-eval/repl-eval! parsed))}
|
|
|
|
-bot-api-call
|
|
{:doc "Make an API call to the local Metabase instance"
|
|
:examples [["./bin/mage -bot-api-call /api/health" "Health check"]
|
|
["./bin/mage -bot-api-call /api/card --api-key mb_admin_apikey" "List cards as admin"]
|
|
["./bin/mage -bot-api-call /api/card --method POST --api-key mb_admin_apikey --body '{\"dataset_query\":{}}'" "Create a card"]]
|
|
:requires [[mage.bot.api-call :as api-call]]
|
|
:arg-schema [:tuple [:string {:name "api-path"}]]
|
|
:options [[nil "--method METHOD" "HTTP method (GET, POST, PUT, DELETE)" :default "GET"]
|
|
[nil "--api-key KEY" "API key for x-api-key header"]
|
|
[nil "--body JSON" "JSON request body"]
|
|
[nil "--pretty BOOL" "Pretty-print JSON (default true)" :default "true"]
|
|
[nil "--raw" "Suppress diagnostic preamble on stderr (pure-body stdout for jq/python piping)"]]
|
|
:task (task! (api-call/api-call! parsed))}
|
|
|
|
-bot-md-to-pdf
|
|
{:doc "Convert a markdown file to PDF using md-to-pdf"
|
|
:examples [["./bin/mage -bot-md-to-pdf report.md" "Convert report.md to report.pdf"]
|
|
["./bin/mage -bot-md-to-pdf .bot/qabot/20260410/report.md" "Convert with full path"]]
|
|
:arg-schema [:tuple [:string {:name "file"}]]
|
|
:task (task!
|
|
(let [md-file (first (:arguments parsed))
|
|
dir (.getParent (java.io.File. ^String md-file))
|
|
fname (.getName (java.io.File. ^String md-file))]
|
|
(babashka.tasks/shell {:dir (or dir ".")} "npx" "-y" "md-to-pdf" fname)))}}}
|