From 8c62eaebaa813a8f512ff9394035fb441ec96135 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:27:22 +0000 Subject: [PATCH 1/4] feat(mise): add fmt mise task for per-language formatting Adds `mise run fmt` and `mise run fmt:` (one per built-in formatter) as a mise-native alternative to `make fmt`. Splits the per-linter-script execution logic out of shell/fmt.sh into a new shell/run-formatter.sh, shared by the existing serial `make fmt` loop and the new per-language mise tasks. The `fmt` aggregator task depends on `fmt:*`, so a downstream repo's own formatter task (file-based or TOML) joins the parallel run automatically without devbase needing to know about it in advance. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ME7x62LMxbvJ5dmyS3i51N --- .mise/tasks/fmt/_default | 3 +++ .mise/tasks/fmt/bash | 9 +++++++ .mise/tasks/fmt/go | 9 +++++++ .mise/tasks/fmt/js | 9 +++++++ .mise/tasks/fmt/jsonnet | 9 +++++++ .mise/tasks/fmt/kubecfg | 9 +++++++ .mise/tasks/fmt/prettier | 9 +++++++ .mise/tasks/fmt/protobuf | 9 +++++++ .mise/tasks/fmt/terraform | 9 +++++++ .mise/tasks/fmt/toml | 9 +++++++ docs/linters.md | 35 ++++++++++++++++++++++++++ shell/fmt.sh | 43 +------------------------------- shell/run-formatter.sh | 52 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 172 insertions(+), 42 deletions(-) create mode 100755 .mise/tasks/fmt/_default create mode 100755 .mise/tasks/fmt/bash create mode 100755 .mise/tasks/fmt/go create mode 100755 .mise/tasks/fmt/js create mode 100755 .mise/tasks/fmt/jsonnet create mode 100755 .mise/tasks/fmt/kubecfg create mode 100755 .mise/tasks/fmt/prettier create mode 100755 .mise/tasks/fmt/protobuf create mode 100755 .mise/tasks/fmt/terraform create mode 100755 .mise/tasks/fmt/toml create mode 100755 shell/run-formatter.sh diff --git a/.mise/tasks/fmt/_default b/.mise/tasks/fmt/_default new file mode 100755 index 000000000..718eb6d8a --- /dev/null +++ b/.mise/tasks/fmt/_default @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +#MISE description="Run all formatters." +#MISE depends=[{ task = "fmt:*", optional = true }] diff --git a/.mise/tasks/fmt/bash b/.mise/tasks/fmt/bash new file mode 100755 index 000000000..b307f7dd2 --- /dev/null +++ b/.mise/tasks/fmt/bash @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats shell scripts." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/bash.sh" diff --git a/.mise/tasks/fmt/go b/.mise/tasks/fmt/go new file mode 100755 index 000000000..540c6c76a --- /dev/null +++ b/.mise/tasks/fmt/go @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats Go source files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/go.sh" diff --git a/.mise/tasks/fmt/js b/.mise/tasks/fmt/js new file mode 100755 index 000000000..dcf59f581 --- /dev/null +++ b/.mise/tasks/fmt/js @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats JavaScript files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/js.sh" diff --git a/.mise/tasks/fmt/jsonnet b/.mise/tasks/fmt/jsonnet new file mode 100755 index 000000000..e3cb04d8e --- /dev/null +++ b/.mise/tasks/fmt/jsonnet @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats Jsonnet files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/jsonnet.sh" diff --git a/.mise/tasks/fmt/kubecfg b/.mise/tasks/fmt/kubecfg new file mode 100755 index 000000000..e4d37c148 --- /dev/null +++ b/.mise/tasks/fmt/kubecfg @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats Kubernetes manifests (no-op; kubecfg has no formatter)." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/kubecfg.sh" diff --git a/.mise/tasks/fmt/prettier b/.mise/tasks/fmt/prettier new file mode 100755 index 000000000..8c4a8a1f5 --- /dev/null +++ b/.mise/tasks/fmt/prettier @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats YAML, JSON, Markdown, and TypeScript files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/prettier.sh" diff --git a/.mise/tasks/fmt/protobuf b/.mise/tasks/fmt/protobuf new file mode 100755 index 000000000..ffc7a4d42 --- /dev/null +++ b/.mise/tasks/fmt/protobuf @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats Protobuf files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/protobuf.sh" diff --git a/.mise/tasks/fmt/terraform b/.mise/tasks/fmt/terraform new file mode 100755 index 000000000..ed570a1bb --- /dev/null +++ b/.mise/tasks/fmt/terraform @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats Terraform files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/terraform.sh" diff --git a/.mise/tasks/fmt/toml b/.mise/tasks/fmt/toml new file mode 100755 index 000000000..25ff56f07 --- /dev/null +++ b/.mise/tasks/fmt/toml @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Formats TOML files." + +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SHELL_DIR="$DIR/../../../shell" + +exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/toml.sh" diff --git a/docs/linters.md b/docs/linters.md index 88d41a705..6036ae260 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -20,3 +20,38 @@ To add a linter place the linter shell script in `scripts/linters/.s The linter will be discovered when globbing `.sh` files run with the built-in linters. Follow the conventions of the existing linter shell scripts when creating the new linter. + +## Running formatters via mise + +`mise run fmt` is the mise-native equivalent of `make fmt`. `mise run fmt:` +(e.g. `mise run fmt:go`, `mise run fmt:bash`) runs a single formatter. Run +`mise tasks ls` to see the full list of `fmt:*` tasks. + +Unlike `make fmt`, which auto-discovers `scripts/linters/*.sh` by globbing, +`mise run fmt` only runs formatters registered as `fmt:` mise tasks. A +project-specific linter (added as above) needs its own `fmt:` task to be +picked up by `mise run fmt`, via either: + +1. A file-based task at `.mise/tasks/fmt/` that delegates to devbase's + shared runner (vendored at `.bootstrap/shell` in your repo): + + ```bash + #!/usr/bin/env bash + #MISE description="Formats files." + + set -euo pipefail + + exec "$MISE_PROJECT_ROOT/.bootstrap/shell/run-formatter.sh" "$MISE_PROJECT_ROOT/scripts/linters/.sh" + ``` + +2. A plain TOML task in your own `mise.toml`, with no dependency on the + `extensions`/`formatter()` convention at all: + + ```toml + [tasks."fmt:"] + description = "Formats files." + run = "your-formatter-command --write ." + ``` + +Both forms are discovered identically by `mise run fmt`, since mise treats +file-based and TOML-defined tasks the same way for task lookup. diff --git a/shell/fmt.sh b/shell/fmt.sh index c58d58e36..cc6d5bdb2 100755 --- a/shell/fmt.sh +++ b/shell/fmt.sh @@ -8,16 +8,10 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" # shellcheck source=./lib/bootstrap.sh source "$DIR/lib/bootstrap.sh" -# shellcheck source=./lib/github.sh -source "$DIR/lib/github.sh" # shellcheck source=./lib/logging.sh source "$DIR/lib/logging.sh" -# shellcheck source=./lib/mise.sh -source "$DIR/lib/mise.sh" # shellcheck source=./lib/shell.sh source "$DIR/lib/shell.sh" -# shellcheck source=./lib/version.sh -source "$DIR/lib/version.sh" # add extra (per project) linters linters=("$DIR/linters"/*.sh) @@ -32,42 +26,7 @@ info "Running formatters" started_at="$(get_time_ms)" for linterScript in "${linters[@]}"; do - - # We use a sub-shell to prevent inheriting - # the changes to functions/variables to the parent - # (this) script - ( - # Note: These are modified by the source'd language file - # extensions are the extensions this linter should run on - extensions=() - - # Why: Dynamic - # shellcheck disable=SC1090 - source "$linterScript" - - matched=false - if [[ "$(find_files_with_extensions "${extensions[@]}" | wc -l | tr -d ' ')" -gt 0 ]]; then - matched=true - fi - - if [[ $matched == "false" ]]; then - exit 0 - fi - - # Note: extensions is set by the linter. - # Why: We're OK with declaring and assigning. - # shellcheck disable=SC2155,SC2001 - extensionsString=$(sed 's/ /,./g' <<<"${extensions[*]}" | sed 's/^/./') - - # show is used by run_command as metadata to be shown along with the command name - show=$extensionsString - - # Set by the language file - if ! formatter; then - error "Formatter failed to run" - exit 1 - fi - ) + "$DIR/run-formatter.sh" "$linterScript" || exit 1 done finished_at="$(get_time_ms)" duration="$((finished_at - started_at))" diff --git a/shell/run-formatter.sh b/shell/run-formatter.sh new file mode 100755 index 000000000..f63662573 --- /dev/null +++ b/shell/run-formatter.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Runs the formatter() function defined by a single linter script (see +# shell/linters/*.sh) against the files it declares via `extensions`. +# +# Used by shell/fmt.sh (in series) and by the per-formatter mise tasks +# under .mise/tasks/fmt/ (which mise may run in parallel). +set -e -o pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +# shellcheck source=./lib/bootstrap.sh +source "$DIR/lib/bootstrap.sh" +# shellcheck source=./lib/github.sh +source "$DIR/lib/github.sh" +# shellcheck source=./lib/logging.sh +source "$DIR/lib/logging.sh" +# shellcheck source=./lib/mise.sh +source "$DIR/lib/mise.sh" +# shellcheck source=./lib/shell.sh +source "$DIR/lib/shell.sh" +# shellcheck source=./lib/version.sh +source "$DIR/lib/version.sh" + +linterScript="${1:-}" +if [[ -z $linterScript ]]; then + fatal "Usage: run-formatter.sh " +fi + +# Note: extensions is set by the linter script sourced below. +extensions=() + +# Why: Dynamic +# shellcheck disable=SC1090 +source "$linterScript" + +if [[ "$(find_files_with_extensions "${extensions[@]}" | wc -l | tr -d ' ')" -eq 0 ]]; then + exit 0 +fi + +# Note: extensions is set by the linter. +# Why: We're OK with declaring and assigning. +# shellcheck disable=SC2155,SC2001 +extensionsString=$(sed 's/ /,./g' <<<"${extensions[*]}" | sed 's/^/./') + +# show is used by run_command as metadata to be shown along with the command name +show=$extensionsString + +# Set by the linter script. +if ! formatter; then + error "Formatter failed to run" + exit 1 +fi From 353bec23a229cb58785c48526efcf255633d8358 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:32:51 +0000 Subject: [PATCH 2/4] fix(mise): drop fmt:kubecfg task since kubecfg has no formatter shell/linters/kubecfg.sh's formatter() is a no-op ({ true; }), so a fmt:kubecfg mise task would claim to format something while doing nothing. make fmt is unaffected: it still discovers and runs kubecfg.sh's no-op formatter as before. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ME7x62LMxbvJ5dmyS3i51N --- .mise/tasks/fmt/kubecfg | 9 --------- 1 file changed, 9 deletions(-) delete mode 100755 .mise/tasks/fmt/kubecfg diff --git a/.mise/tasks/fmt/kubecfg b/.mise/tasks/fmt/kubecfg deleted file mode 100755 index e4d37c148..000000000 --- a/.mise/tasks/fmt/kubecfg +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Formats Kubernetes manifests (no-op; kubecfg has no formatter)." - -set -euo pipefail - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -SHELL_DIR="$DIR/../../../shell" - -exec "$SHELL_DIR/run-formatter.sh" "$SHELL_DIR/linters/kubecfg.sh" From 21016769742b54a00a667371088b973f6aaf4a47 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:38:58 +0000 Subject: [PATCH 3/4] refactor(mise): build extensionsString without sed in run-formatter.sh Replaces the sed-piped extensionsString construction with bash parameter expansion, dropping the SC2155/SC2001 shellcheck-disable comment it needed. Output is unchanged (e.g. ".sh,.bash,.bats"). The remaining SC1090 disable on `source "$linterScript"` stays: the path is a runtime argument, so shellcheck genuinely can't resolve it statically, and that's inherent to run-formatter.sh being a generic driver for any linter script rather than a fixable code smell. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ME7x62LMxbvJ5dmyS3i51N --- shell/run-formatter.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell/run-formatter.sh b/shell/run-formatter.sh index f63662573..700a0ba73 100755 --- a/shell/run-formatter.sh +++ b/shell/run-formatter.sh @@ -38,9 +38,10 @@ if [[ "$(find_files_with_extensions "${extensions[@]}" | wc -l | tr -d ' ')" -eq fi # Note: extensions is set by the linter. -# Why: We're OK with declaring and assigning. -# shellcheck disable=SC2155,SC2001 -extensionsString=$(sed 's/ /,./g' <<<"${extensions[*]}" | sed 's/^/./') +extensionsPrefixed=("${extensions[@]/#/.}") +IFS=, +extensionsString="${extensionsPrefixed[*]}" +unset IFS # show is used by run_command as metadata to be shown along with the command name show=$extensionsString From c4e27e61eda6fb75517c89dc63a7527800beb23a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:42:28 +0000 Subject: [PATCH 4/4] docs(linters): use scripts/shell-wrapper.sh in the fmt: example scripts/shell-wrapper.sh is the existing, already-documented way consumer repos reach devbase's vendored shell scripts (it's what make fmt itself uses), rather than hardcoding the .bootstrap/shell vendor path directly. Verified end-to-end in a simulated consumer repo (devbase vendored under .bootstrap/, real service.yaml). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ME7x62LMxbvJ5dmyS3i51N --- docs/linters.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/linters.md b/docs/linters.md index 6036ae260..4ffc438b1 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -33,7 +33,8 @@ project-specific linter (added as above) needs its own `fmt:` task to be picked up by `mise run fmt`, via either: 1. A file-based task at `.mise/tasks/fmt/` that delegates to devbase's - shared runner (vendored at `.bootstrap/shell` in your repo): + shared runner via `scripts/shell-wrapper.sh` (the same wrapper `make fmt` + itself uses to reach devbase's vendored shell scripts): ```bash #!/usr/bin/env bash @@ -41,7 +42,7 @@ picked up by `mise run fmt`, via either: set -euo pipefail - exec "$MISE_PROJECT_ROOT/.bootstrap/shell/run-formatter.sh" "$MISE_PROJECT_ROOT/scripts/linters/.sh" + exec "$MISE_PROJECT_ROOT/scripts/shell-wrapper.sh" run-formatter.sh "$MISE_PROJECT_ROOT/scripts/linters/.sh" ``` 2. A plain TOML task in your own `mise.toml`, with no dependency on the