From 9cb6ad985efd5c812c6f1a130816b5f2950a6ea9 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Thu, 25 Jun 2026 13:07:18 -0300 Subject: [PATCH] =?UTF-8?q?feat(lint):=20add=20mise-usage-examples=20rule?= =?UTF-8?q?=20=E2=80=94=20enforce=20#USAGE=20example=20for=20public=20task?= =?UTF-8?q?s=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mise/tasks/lint/_default | 1 + .mise/tasks/lint/bats-test-helper | 1 + .mise/tasks/lint/bats-test-task | 1 + .mise/tasks/lint/caller-pwd-contract | 1 + .mise/tasks/lint/github-actions | 2 + .mise/tasks/lint/gum-table | 1 + .mise/tasks/lint/mcr-scope | 1 + .mise/tasks/lint/mise-settings | 2 + .mise/tasks/lint/mise-usage-examples | 96 +++++++++++++++ .mise/tasks/lint/shellcheck | 1 + .mise/tasks/migrate/task-pattern | 2 + .mise/tasks/pre-commit | 2 + .mise/tasks/scan | 1 + AGENTS.md | 116 ++++++++++++++++++ .../fixtures/boolean-only/.mise/tasks/verbose | 5 + .../fixtures/clean/.mise/tasks/deploy | 6 + .../fixtures/clean/.mise/tasks/greet | 6 + .../fixtures/dirty/.mise/tasks/deploy | 5 + .../fixtures/dirty/.mise/tasks/greet | 5 + .../fixtures/empty/.mise/tasks/.gitkeep | 0 .../fixtures/hidden/.mise/tasks/internal-tool | 6 + .../fixtures/ignored-inline/.mise/tasks/greet | 6 + .../fixtures/ignored-repo/.mise/tasks/deploy | 5 + .../fixtures/ignored-repo/mise.toml | 4 + .../fixtures/mixed/.mise/tasks/broken-task | 5 + .../fixtures/mixed/.mise/tasks/greet | 6 + .../fixtures/no-args/.mise/tasks/helper | 4 + .../mise-usage-examples.bats | 91 ++++++++++++++ 28 files changed, 382 insertions(+) create mode 100755 .mise/tasks/lint/mise-usage-examples create mode 100644 AGENTS.md create mode 100644 test/lint/mise-usage-examples/fixtures/boolean-only/.mise/tasks/verbose create mode 100644 test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/deploy create mode 100644 test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/greet create mode 100644 test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/deploy create mode 100644 test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/greet create mode 100644 test/lint/mise-usage-examples/fixtures/empty/.mise/tasks/.gitkeep create mode 100644 test/lint/mise-usage-examples/fixtures/hidden/.mise/tasks/internal-tool create mode 100644 test/lint/mise-usage-examples/fixtures/ignored-inline/.mise/tasks/greet create mode 100644 test/lint/mise-usage-examples/fixtures/ignored-repo/.mise/tasks/deploy create mode 100644 test/lint/mise-usage-examples/fixtures/ignored-repo/mise.toml create mode 100644 test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/broken-task create mode 100644 test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/greet create mode 100644 test/lint/mise-usage-examples/fixtures/no-args/.mise/tasks/helper create mode 100644 test/lint/mise-usage-examples/mise-usage-examples.bats diff --git a/.mise/tasks/lint/_default b/.mise/tasks/lint/_default index 297abc7..5e5f6fb 100755 --- a/.mise/tasks/lint/_default +++ b/.mise/tasks/lint/_default @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Run configured codebase convention lints" #USAGE arg "[target]" default="." help="Path to the target repository" +#USAGE example "codebase lint /path/to/repo" header="Lint a repo" set -euo pipefail diff --git a/.mise/tasks/lint/bats-test-helper b/.mise/tasks/lint/bats-test-helper index 39d9141..cf74fa7 100755 --- a/.mise/tasks/lint/bats-test-helper +++ b/.mise/tasks/lint/bats-test-helper @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Flag direct invocation of .mise/tasks/* scripts from BATS tests (call the tool, not the script)" #USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:bats-test-helper ." header="Check for direct script invocation in BATS tests" set -euo pipefail diff --git a/.mise/tasks/lint/bats-test-task b/.mise/tasks/lint/bats-test-task index cdd03e6..5c70070 100755 --- a/.mise/tasks/lint/bats-test-task +++ b/.mise/tasks/lint/bats-test-task @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Enforce the canonical BATS test-task shape in .mise/tasks/test" #USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:bats-test-task ." header="Check BATS test task shape" set -euo pipefail diff --git a/.mise/tasks/lint/caller-pwd-contract b/.mise/tasks/lint/caller-pwd-contract index 919a896..cd1844a 100755 --- a/.mise/tasks/lint/caller-pwd-contract +++ b/.mise/tasks/lint/caller-pwd-contract @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Check shiv caller-cwd environment variable contract" #USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:caller-pwd-contract ." header="Check caller-pwd contract" set -euo pipefail diff --git a/.mise/tasks/lint/github-actions b/.mise/tasks/lint/github-actions index ae3e426..163ff94 100755 --- a/.mise/tasks/lint/github-actions +++ b/.mise/tasks/lint/github-actions @@ -2,6 +2,8 @@ #MISE description="Lint GitHub Actions workflows and create a KKL default workflow when missing" #USAGE arg "…" help="Paths to codebases to check (one or more)" #USAGE flag "--fix" help="Create a default GitHub Actions workflow when missing" +#USAGE example "codebase lint:github-actions ." header="Lint GitHub Actions workflows" +#USAGE example "codebase lint:github-actions . --fix" header="Create default workflow" set -euo pipefail diff --git a/.mise/tasks/lint/gum-table b/.mise/tasks/lint/gum-table index b1fa03c..fabf8bf 100755 --- a/.mise/tasks/lint/gum-table +++ b/.mise/tasks/lint/gum-table @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Detect manual table formatting that should use gum table" #USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:gum-table ." header="Check for manual table formatting" set -euo pipefail diff --git a/.mise/tasks/lint/mcr-scope b/.mise/tasks/lint/mcr-scope index e1235e4..ddc333a 100755 --- a/.mise/tasks/lint/mcr-scope +++ b/.mise/tasks/lint/mcr-scope @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Forbid MISE_CONFIG_ROOT in test/ and lib/ (use BATS_TEST_DIRNAME or BASH_SOURCE instead)" #USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:mcr-scope ." header="Check for MISE_CONFIG_ROOT misuse in test/lib" set -euo pipefail diff --git a/.mise/tasks/lint/mise-settings b/.mise/tasks/lint/mise-settings index aa41c08..f01809c 100755 --- a/.mise/tasks/lint/mise-settings +++ b/.mise/tasks/lint/mise-settings @@ -2,6 +2,8 @@ #MISE description="Check that mise.toml has required settings" #USAGE arg "…" help="Paths to codebases to check (one or more)" #USAGE flag "--fix" help="Add missing settings to mise.toml" +#USAGE example "codebase lint:mise-settings ." header="Check mise.toml settings" +#USAGE example "codebase lint:mise-settings . --fix" header="Fix missing settings" set -euo pipefail diff --git a/.mise/tasks/lint/mise-usage-examples b/.mise/tasks/lint/mise-usage-examples new file mode 100755 index 0000000..110208b --- /dev/null +++ b/.mise/tasks/lint/mise-usage-examples @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +#MISE description="Enforce #USAGE example directives for public argument-bearing mise tasks" +#USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:mise-usage-examples ." header="Check #USAGE example coverage" + +set -euo pipefail + +# shellcheck source=../../../lib/shell-files.sh +source "$MISE_CONFIG_ROOT/lib/shell-files.sh" + +# Rationale: `mise run --help` shows #USAGE example lines as usage +# examples. Tasks that declare #USAGE arg or #USAGE flag directives give +# users a parameter reference but no concrete invocation examples — making +# the --help output incomplete. +# +# The bats-test-task rule already enforces this for .mise/tasks/test; +# this rule generalises the check to all public tasks. +# +# Legitimate exceptions (e.g. internal-only tasks that happen to have +# #USAGE declarations for documentation) opt out via: +# mise.toml: codebase:ignore mise-usage-examples +# inline comment: # codebase:ignore mise-usage-examples -- reason +# +# Hidden tasks (#MISE hide=true) are always skipped — they aren't shown +# in --help anyway, so missing examples are harmless. + +IFS=' ' read -ra TARGETS <<< "${usage_targets}" + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "ERROR: at least one target is required" >&2 + exit 1 +fi + +# Resolve relative paths against CALLER_PWD (see lib/shell-files.sh). +for i in "${!TARGETS[@]}"; do + TARGETS[$i]=$(resolve_target "${TARGETS[$i]}") +done + +failures=0 + +for target in "${TARGETS[@]}"; do + if [[ ! -e "$target" ]]; then + echo "ERROR: target does not exist: $target" >&2 + exit 1 + fi + + name=$(basename "$target") + + # File-level ignore via mise.toml + toml="$target/mise.toml" + if [[ -f "$toml" ]] && rg -q 'codebase:ignore mise-usage-examples' "$toml"; then + echo "SKIP $name (codebase:ignore)" + continue + fi + + tasks_dir="$target/.mise/tasks" + if [[ ! -d "$tasks_dir" ]]; then + echo "OK $name (no .mise/tasks — nothing to check)" + continue + fi + + target_failures=0 + + while IFS= read -r -d '' task; do + task_rel="${task#"$tasks_dir/"}" + task_rel="${task_rel#/}" + + # Skip hidden tasks — they aren't shown in --help + if rg -q '^#MISE hide=true' "$task"; then + continue + fi + + # Skip tasks with no user-facing args/flags (no USAGE surface) + if ! rg -q '^#USAGE (arg|flag) ' "$task"; then + continue + fi + + # Skip inline-ignored files + if rg -q 'codebase:ignore mise-usage-examples' "$task"; then + continue + fi + + # Check for at least one #USAGE example directive + if ! rg -q '^#USAGE example ' "$task"; then + echo "FAIL $name $task_rel: missing #USAGE example directives (has args/flags but no examples)" + target_failures=$((target_failures + 1)) + fi + done < <(find "$tasks_dir" -maxdepth 1 -type f -print0) + + if [[ "$target_failures" -eq 0 ]]; then + echo "OK $name (.mise/tasks tasks all have #USAGE examples)" + fi + failures=$((failures + target_failures)) +done + +exit "$failures" \ No newline at end of file diff --git a/.mise/tasks/lint/shellcheck b/.mise/tasks/lint/shellcheck index 222f0e4..801bd06 100755 --- a/.mise/tasks/lint/shellcheck +++ b/.mise/tasks/lint/shellcheck @@ -1,6 +1,7 @@ #!/usr/bin/env bash #MISE description="Run shellcheck against shell files in a codebase" #USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE example "codebase lint:shellcheck ." header="Run shellcheck on repo" set -euo pipefail diff --git a/.mise/tasks/migrate/task-pattern b/.mise/tasks/migrate/task-pattern index 033ead4..798b079 100755 --- a/.mise/tasks/migrate/task-pattern +++ b/.mise/tasks/migrate/task-pattern @@ -2,6 +2,8 @@ #MISE description="Migrate mise run calls to _task pattern (or reverse)" #USAGE arg "" help="Path to the codebase to migrate" #USAGE flag "--reverse" help="Reverse: _task back to mise run" +#USAGE example "codebase migrate /path/to/repo" header="Migrate to _task pattern" +#USAGE example "codebase migrate /path/to/repo --reverse" header="Reverse migration to mise run" set -euo pipefail diff --git a/.mise/tasks/pre-commit b/.mise/tasks/pre-commit index c009a4a..1454b10 100755 --- a/.mise/tasks/pre-commit +++ b/.mise/tasks/pre-commit @@ -2,6 +2,8 @@ #MISE description="Install or check codebase pre-commit hook" #USAGE flag "--revert" help="Remove the codebase pre-commit hook" #USAGE flag "--check" help="Check if hook is installed and current (no changes)" +#USAGE example "codebase pre-commit" header="Install the pre-commit hook" +#USAGE example "codebase pre-commit --check" header="Check hook installation" set -euo pipefail diff --git a/.mise/tasks/scan b/.mise/tasks/scan index 2c512e7..f4aa12c 100755 --- a/.mise/tasks/scan +++ b/.mise/tasks/scan @@ -4,6 +4,7 @@ #USAGE flag "-p --pattern " required=#true help="ast-grep pattern to search for" #USAGE flag "-l --lang " help="Language (default: bash)" #USAGE flag "-e --exclude " help="Glob patterns to exclude (space-separated)" +#USAGE example "codebase scan . -p 'some pattern' -l bash" header="Scan for AST patterns" set -euo pipefail diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..55f0bc7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,116 @@ +# AGENTS.md — codebase + +Structural code analysis, linting & migrations for KnickKnackLabs repos. + +--- + +## Commands + +All commands are mise tasks in `.mise/tasks/`: + +| Command | Description | +|---------|-------------| +| `mise run lint [target]` | Run configured codebase convention lints against a repo | +| `mise run scan -p [-l lang] [-e glob]` | Scan codebase for AST pattern matches via ast-grep | +| `mise run migrate ` | Migrate `mise run` calls to `_task` pattern (or reverse with `--reverse`) | +| `mise run pre-commit [--check] [--revert]` | Install/check/remove codebase pre-commit hook | +| `mise run test [args...]` | Run BATS test suite | + +--- + +## Environment + +| Key | Value | +|---|---| +| Repo | `$HOME/codebase` | +| Fork | `https://github.com/olavostauros/codebase` | +| Upstream | `https://github.com/KnickKnackLabs/codebase` | +| Tools | `bats`, `ast-grep`, `shellcheck`, `actionlint`, `ripgrep`, `fd` (managed via mise) | + +--- + +## Usage + +### Lint a repo + +```bash +cd /path/to/target-repo +codebase lint +# or from anywhere: +mise run lint /path/to/target-repo +``` + +Lint rules are configured in the target repo's `mise.toml` under `[_.codebase]`: + +```toml +[_.codebase] +lint = ["mise-settings", "gum-table", "shellcheck"] +``` + +### Available lint rules + +| Rule | Description | +|------|-------------| +| `mise-settings` | Check that `mise.toml` has required settings (`quiet=true`, `task_output=interleave`) | +| `gum-table` | Detect manual table formatting using `column -t` or `printf %-Ns` that should use `gum table` | +| `shellcheck` | Run shellcheck against shell files in a codebase | +| `bats-test-task` | Enforce the canonical BATS test-task shape in `.mise/tasks/test` | +| `bats-test-helper` | Flag direct invocation of `.mise/tasks/*` scripts from BATS tests | +| `mcr-scope` | Forbid `MISE_CONFIG_ROOT` in `test/` and `lib/` | +| `caller-pwd-contract` | Check shiv caller-cwd environment variable contract | +| `github-actions` | Lint GitHub Actions workflows and create a KKL default workflow when missing | +| `or-true` | Classify risky unannotated `\|\| true` / `\|\| :` failure suppression | +| `mise-usage-examples` | Enforce `#USAGE example` directives for public argument-bearing `mise` tasks | + +### Scan for patterns + +```bash +codebase scan /path/to/repo -p "some pattern" -l bash +``` + +### Install pre-commit hook + +```bash +cd /path/to/target-repo +codebase pre-commit +``` + +### Run tests + +```bash +mise run test +mise run test lint/bats-test-task # specific suite +``` + +--- + +## Test structure + +Tests live in `test/` organized by feature area: + +| Directory | Tests | +|-----------|-------| +| `test/lib/` | Unit tests for shared lib functions | +| `test/lint/` | Lint rule tests (one subdir per rule) | +| `test/migrations/` | Migration task tests | +| `test/pre-commit/` | Pre-commit hook tests | +| `test/scan/` | Scan task tests (includes AST fixtures) | + +--- + +## Shared libs + +| File | Purpose | +|------|---------| +| `lib/codebase-config.sh` | Repo resolution, lint rule discovery from `mise.toml` | +| `lib/shell-files.sh` | File discovery, path resolution (`resolve_target`) | + +--- + +## Conventions + +- Bash-first (no Node/Python runtime dependencies) +- `test/test_helper.bash` provides common test bootstrapping +- CI runs on ubuntu-latest and macos-latest via GitHub Actions +- Lint rules are simple bash scripts that inspect repo structure +- Comments use single-line section headers (`# === Section title ===`), never multi-line ruler blocks \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/boolean-only/.mise/tasks/verbose b/test/lint/mise-usage-examples/fixtures/boolean-only/.mise/tasks/verbose new file mode 100644 index 0000000..4cd81d0 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/boolean-only/.mise/tasks/verbose @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Enable verbose output" +#USAGE flag "--verbose" help="Enable verbose output" +set -euo pipefail +echo "Verbose mode: ${usage_verbose:-off}" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/deploy b/test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/deploy new file mode 100644 index 0000000..9f2acf7 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/deploy @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Deploy the app" +#USAGE flag "--env " help="Target environment" +#USAGE example "mise run deploy --env staging" header="Deploy to staging" +set -euo pipefail +echo "Deploying to ${usage_env:-production}..." \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/greet b/test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/greet new file mode 100644 index 0000000..8bd1880 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/clean/.mise/tasks/greet @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Greet the user" +#USAGE arg "" help="Name to greet" +#USAGE example "mise run greet Alice" header="Greet Alice" +set -euo pipefail +echo "Hello, ${usage_name}!" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/deploy b/test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/deploy new file mode 100644 index 0000000..9f7d3fa --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/deploy @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Deploy the app" +#USAGE flag "--env " help="Target environment" +set -euo pipefail +echo "Deploying to ${usage_env:-production}..." \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/greet b/test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/greet new file mode 100644 index 0000000..1236be6 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/dirty/.mise/tasks/greet @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Greet the user" +#USAGE arg "" help="Name to greet" +set -euo pipefail +echo "Hello, ${usage_name}!" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/empty/.mise/tasks/.gitkeep b/test/lint/mise-usage-examples/fixtures/empty/.mise/tasks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/lint/mise-usage-examples/fixtures/hidden/.mise/tasks/internal-tool b/test/lint/mise-usage-examples/fixtures/hidden/.mise/tasks/internal-tool new file mode 100644 index 0000000..7897395 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/hidden/.mise/tasks/internal-tool @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Internal tool — hidden from --help" +#MISE hide=true +#USAGE arg "" help="Auth token" +set -euo pipefail +echo "Internal task running with token: ${usage_token}" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/ignored-inline/.mise/tasks/greet b/test/lint/mise-usage-examples/fixtures/ignored-inline/.mise/tasks/greet new file mode 100644 index 0000000..5dcd37f --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/ignored-inline/.mise/tasks/greet @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Greet the user" +#USAGE arg "" help="Name to greet" +# codebase:ignore mise-usage-examples -- intentionally bare, this is an internal wrapper +set -euo pipefail +echo "Hello, ${usage_name}!" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/ignored-repo/.mise/tasks/deploy b/test/lint/mise-usage-examples/fixtures/ignored-repo/.mise/tasks/deploy new file mode 100644 index 0000000..9f7d3fa --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/ignored-repo/.mise/tasks/deploy @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Deploy the app" +#USAGE flag "--env " help="Target environment" +set -euo pipefail +echo "Deploying to ${usage_env:-production}..." \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/ignored-repo/mise.toml b/test/lint/mise-usage-examples/fixtures/ignored-repo/mise.toml new file mode 100644 index 0000000..aa4ad5d --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/ignored-repo/mise.toml @@ -0,0 +1,4 @@ +[_.codebase] +lint = ["mise-settings"] + +codebase:ignore mise-usage-examples \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/broken-task b/test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/broken-task new file mode 100644 index 0000000..3c566e8 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/broken-task @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Broken task — missing example" +#USAGE arg "" help="Path to process" +set -euo pipefail +echo "Processing ${usage_path}..." \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/greet b/test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/greet new file mode 100644 index 0000000..8bd1880 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/mixed/.mise/tasks/greet @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Greet the user" +#USAGE arg "" help="Name to greet" +#USAGE example "mise run greet Alice" header="Greet Alice" +set -euo pipefail +echo "Hello, ${usage_name}!" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/fixtures/no-args/.mise/tasks/helper b/test/lint/mise-usage-examples/fixtures/no-args/.mise/tasks/helper new file mode 100644 index 0000000..1302be1 --- /dev/null +++ b/test/lint/mise-usage-examples/fixtures/no-args/.mise/tasks/helper @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#MISE description="A simple helper — no USAGE directives at all" +set -euo pipefail +echo "Hello" \ No newline at end of file diff --git a/test/lint/mise-usage-examples/mise-usage-examples.bats b/test/lint/mise-usage-examples/mise-usage-examples.bats new file mode 100644 index 0000000..79fe846 --- /dev/null +++ b/test/lint/mise-usage-examples/mise-usage-examples.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats +# Tests for lint:mise-usage-examples rule + +load ../../test_helper + +setup() { + FIXTURES="$BATS_TEST_DIRNAME/fixtures" +} + +# === Detection === + +@test "mise-usage-examples: passes on a repo where all tasks with args have examples" { + run codebase lint:mise-usage-examples "$FIXTURES/clean" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"clean"* ]] +} + +@test "mise-usage-examples: flags a task that has #USAGE arg but no #USAGE example" { + run codebase lint:mise-usage-examples "$FIXTURES/dirty" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty"* ]] + [[ "$output" == *"missing #USAGE example"* ]] +} + +@test "mise-usage-examples: flags a task that has #USAGE flag but no #USAGE example" { + run codebase lint:mise-usage-examples "$FIXTURES/dirty" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"dirty"* ]] + [[ "$output" == *"deploy"*"missing #USAGE example"* ]] +} + +@test "mise-usage-examples: flags boolean-only flags (no placeholder) without examples" { + run codebase lint:mise-usage-examples "$FIXTURES/boolean-only" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"boolean-only"* ]] + [[ "$output" == *"verbose"*"missing #USAGE example"* ]] +} + +@test "mise-usage-examples: skips hidden tasks (#MISE hide=true) even without examples" { + run codebase lint:mise-usage-examples "$FIXTURES/hidden" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"hidden"* ]] + [[ "$output" != *"internal-tool"* ]] +} + +@test "mise-usage-examples: skips tasks with no #USAGE arg/flag at all" { + run codebase lint:mise-usage-examples "$FIXTURES/no-args" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"no-args"* ]] +} + +@test "mise-usage-examples: respects inline codebase:ignore comment" { + run codebase lint:mise-usage-examples "$FIXTURES/ignored-inline" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"ignored-inline"* ]] +} + +@test "mise-usage-examples: respects repo-level codebase:ignore in mise.toml" { + run codebase lint:mise-usage-examples "$FIXTURES/ignored-repo" + [ "$status" -eq 0 ] + [[ "$output" == *"SKIP"*"ignored-repo (codebase:ignore)"* ]] +} + +@test "mise-usage-examples: passes on a repo with no .mise/tasks directory" { + run codebase lint:mise-usage-examples "$FIXTURES/no-tasks" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"no-tasks"* ]] +} + +@test "mise-usage-examples: passes on empty repo (tasks dir exists but is empty)" { + run codebase lint:mise-usage-examples "$FIXTURES/empty" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"empty"* ]] +} + +@test "mise-usage-examples: flags only the offending task when others are clean" { + run codebase lint:mise-usage-examples "$FIXTURES/mixed" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"mixed"* ]] + # The failing task + [[ "$output" == *"broken-task"* ]] + # The clean task should not appear as FAIL + [[ "$output" != *"greet"* ]] +} + +@test "mise-usage-examples: flag output includes the task path relative to .mise/tasks" { + run codebase lint:mise-usage-examples "$FIXTURES/dirty" + [ "$status" -ne 0 ] + [[ "$output" == *"greet"*"missing #USAGE example"* ]] + [[ "$output" == *"deploy"*"missing #USAGE example"* ]] +} \ No newline at end of file