{: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) ;; Let Mage use the same module-resolution rules as the Kondo hooks. :paths ["mage/src" "mage/test" "bin/lint-migrations-file/src" "dev/src" ".clj-kondo/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/_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))} openapi-diff {:doc "Semantic diff of two OpenAPI specs, classified breaking-first, for the API changelog" :examples [["./bin/mage openapi-diff old.json new.json" "Full diff, breaking changes first"] ["./bin/mage openapi-diff old.json new.json --severity breaking" "Only what needs a changelog entry"] ["./bin/mage openapi-diff --refs release-x.63.x release-x.64.x --severity breaking" "Generate both specs from source, then diff"] ["./bin/mage openapi-diff --refs --committed release-x.63.x master" "Use each ref's committed spec (fast, may be stale)"] ["./bin/mage openapi-diff --refs --grouped release-x.63.x master --severity breaking" "One line per distinct change, widest blast radius first"]] :arg-schema [:tuple [:string {:name "old" :description "Path to the older openapi.json, or a git ref with --refs"}] [:string {:name "new" :description "Path to the newer openapi.json, or a git ref with --refs"}]] :options [["-s" "--severity LEVEL" "Only show findings at or above LEVEL: breaking or additive"] ["-r" "--refs" "Treat the arguments as git refs and generate each spec from its source"] ["-c" "--committed" "With --refs, read each ref's committed spec instead of generating (fast, may be stale)"] ["-g" "--grouped" "Collapse identical findings across endpoints: one line per distinct change"]] :requires [[mage.openapi-diff]] :task (task! (mage.openapi-diff/cli-diff parsed))} openapi-staleness {:doc "Is the committed OpenAPI spec current relative to the API source? (a stale spec under-reports)" :examples [["./bin/mage openapi-staleness"]] :arg-schema [:tuple] :requires [[mage.openapi-diff]] :task (task! (mage.openapi-diff/cli-staleness 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)))} modules-tree {:doc "Print the module hierarchy, nesting enterprise extensions under their OSS module" :examples [["./bin/mage modules-tree" "Print the full tree; * marks a custom :ns-prefix"] ["./bin/mage modules-tree --nested-only" "Print only roots with nested modules"] ["./bin/mage modules-tree --prefixes" "Show custom prefixes instead of *"] ["./bin/mage modules-tree --html --output modules.html" "Write the interactive explorer"]] :options [["-n" "--nested-only" "Only print modules that have nested children"] ["-p" "--prefixes" "Print the :ns-prefix instead of the star"] ["-H" "--html" "Emit the interactive HTML explorer instead of the text tree"] ["-o" "--output FILE" "With --html, write to FILE instead of stdout"] ["-S" "--no-stats" "With --html, skip the slower per-module source metrics"]] :requires [[mage.modules]] :task (task! (mage.modules/cli-print-module-tree 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))))} project-tests {:doc "Run project-level checks." :examples [["./bin/mage project-tests" "Run every suite"] ["./bin/mage project-tests backend" "Run backend checks"] ["./bin/mage project-tests migrations" "Run migration checks"] ["./bin/mage project-tests modules" "Run module checks"] ["./bin/mage project-tests ratchets" "Check the repository's kondo suppression ratchets"]] :arg-schema [:or [:tuple] [:tuple [:enum {:name "suite"} "backend" "migrations" "modules" "ratchets"]]] :requires [[mage.project-tests :as project-tests]] :task (task! (if-let [suite (first (:arguments parsed))] (project-tests/run! suite) (project-tests/run!)))} kondo-ratchets {:doc "Validate the kondo and module ratchet files against the source tree (about half a minute, most of it a kondo run over the files with a discouraged-var or discouraged-namespace ignore)." :examples [["./bin/mage kondo-ratchets" "Fail on an invalid ratchets file, an over-budget suppression, or a missing justification comment"]] :requires [[dev.kondo-ratchet] [mage.kondo-ratchet]] :task (task! (dev.kondo-ratchet/check {:attribute mage.kondo-ratchet/attribute-occurrences!}))} kondo-ratchets-shrink {:doc "Lower kondo and module budgets to match the source tree. This is the only Mage task that writes the ratchet files." :examples [["./bin/mage kondo-ratchets-shrink" "Lower budgets to current counts without raising over-budget entries"] ["./bin/mage kondo-ratchets-shrink --seed :some-linter" "Set :some-linter's budget to its current count when introducing the linter"] ["./bin/mage kondo-ratchets-shrink --seed :discouraged-var/clojure.core/println" "Set one discouraged symbol's budget to its current count"] ["./bin/mage kondo-ratchets-shrink --seed :discouraged-var" "Set every discouraged-var symbol's budget to its current count"]] :options [["-s" "--seed LINTER" "Set LINTER's budget to the actual count (may add or raise it); :discouraged-var/SYMBOL seeds one discouraged symbol, a bare :discouraged-var/:discouraged-namespace all of them"]] :requires [[dev.kondo-ratchet] [mage.kondo-ratchet]] :task (task! (dev.kondo-ratchet/fix! (assoc (:options parsed) :attribute mage.kondo-ratchet/attribute-occurrences!)))} 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 `-` -ratchets-merge {:doc "Internal three-way merge for ratchet files. Called by bin/merge-kondo-ratchets." :arg-schema [:tuple [:enum {:name "kind"} "kondo" "test" "modules"] [:string {:name "base" :description "Ratchets file from the merge base"}] [:string {:name "ours" :description "Git stage 2: ratchets file from the branch receiving the changes"}] [:string {:name "theirs" :description "Git stage 3: ratchets file from the commits being applied"}] [:string {:name "output" :description "Path to write the merged ratchets file"}]] :requires [[dev.kondo-ratchet]] :task (task! (let [[kind base-path ours-path theirs-path output-path] (:arguments parsed) modules? (= kind "modules") read-stage (fn [path] (if modules? (binding [dev.kondo-ratchet/*module-ratchets-file* path] (dev.kondo-ratchet/read-module-ratchets)) (binding [dev.kondo-ratchet/*ratchets-file* path] (dev.kondo-ratchet/read-ratchets)))) [base ours theirs] (map read-stage [base-path ours-path theirs-path])] (if modules? (spit output-path (dev.kondo-ratchet/render-module-ratchets (dev.kondo-ratchet/merge-module-ratchets base ours theirs))) (let [merged (dev.kondo-ratchet/merge-ratchets base ours theirs) render (if (= kind "test") dev.kondo-ratchet/render-test dev.kondo-ratchet/render)] (spit output-path (if (dev.kondo-ratchet/disabled? merged) (slurp ours-path) (render merged))))) (println (str "merged ratchets into " output-path))))} -kondo-ratchets-pr-body {:doc "Render the body for an automated ratchet-shrink PR." :arg-schema [:tuple [:string {:name "before" :description "Ratchets file before shrinking"}] [:string {:name "after" :description "Ratchets file after shrinking"}] [:string {:name "modules-before" :description "Module ratchets before shrinking"}] [:string {:name "modules-after" :description "Module ratchets after shrinking"}] [:string {:name "test-before" :description "Test ratchets file before shrinking"}] [:string {:name "test-after" :description "Test ratchets file after shrinking"}] [:string {:name "workflow-url" :description "URL of the workflow run"}]] :requires [[dev.kondo-ratchet]] :task (task! (let [[before after modules-before modules-after test-before test-after workflow-url] (:arguments parsed)] (print (dev.kondo-ratchet/shrink-pr-body-from-files {:before before :after after :modules-before modules-before :modules-after modules-after :test-before test-before :test-after test-after} workflow-url))))} -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 --only-driver=bigquery" "Run only the BigQuery job, whatever the other rules say"] ["./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"] ["0" "--only-driver=DRIVER was passed" "RUN/SKIP" "only that one driver runs"] ["1" "Global force-run" "RUN" "master/release branch or ci:run-all label"] ["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"] ["6" "Cloud driver + ci:run-all-cloud-drivers" "RUN" "ci:run-all-cloud-drivers label"] ["7" "Cloud driver + query-processor updated" "RUN" "query-processor module updated"] ["8" "Cloud driver + driver deps affected" "RUN" "driver module affected by shared code changes"] ["9" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"] ["10" (str (pr-str driver-triggers) " module deps affected") "RUN" (str "any of " (pr-str driver-triggers) " affected by module changes")] ["11" "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" "\n\n## Labels of Interest\n\n" "These PR labels influence driver test decisions:\n\n" " ci:run-all-drivers Force-run ALL drivers\n" " ci:run-DRIVER Force-run a specific driver, e.g. ci:run-mysql-mariadb\n" " ci:run-all-cloud-drivers Run all cloud drivers (athena, bigquery, databricks, redshift, snowflake)\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 "--only-driver DRIVER" "Run only this driver's job, ignoring every other rule"] [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 '^+$' 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)))}}}