Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .mise/tasks/lint/_default
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
#MISE description="Run configured codebase convention lints"
#USAGE arg "[target]" default="." help="Path to the target repository"

set -euo pipefail

# shellcheck source=../../../lib/codebase-config.sh
source "$MISE_CONFIG_ROOT/lib/codebase-config.sh"

TARGET_ARG="${usage_target:-.}"

if ! REPO_ROOT=$(codebase_resolve_repo "$TARGET_ARG"); then
exit 1
fi

TOML="$REPO_ROOT/mise.toml"
if [[ ! -f "$TOML" ]]; then
echo "ERROR: no mise.toml found in $REPO_ROOT" >&2
exit 1
fi

RULES=()
while IFS= read -r rule; do
[[ -n "$rule" ]] && RULES+=("$rule")
done < <(codebase_configured_lint_rules "$REPO_ROOT")

if [[ ${#RULES[@]} -eq 0 ]]; then
echo "ERROR: no lint rules configured in $TOML" >&2
echo "Add to mise.toml:" >&2
echo ' [_.codebase]' >&2
echo ' lint = ["mise-settings", "gum-table"]' >&2
exit 1
fi

failures=0
FAILED=()

for rule in "${RULES[@]}"; do
rule_target=$(codebase_target_for_rule "$REPO_ROOT" "$rule")
echo "codebase: lint:$rule $rule_target"

output=""
if output=$(mise run -q "lint:$rule" "$rule_target" 2>&1); then
status=0
else
status=$?
fi

if [[ -n "$output" ]]; then
printf '%s\n' "$output"
fi

if [[ "$status" -ne 0 ]]; then
failures=$((failures + 1))
FAILED+=("lint:$rule ($rule_target) exited $status")
fi
done

if [[ "$failures" -gt 0 ]]; then
echo "" >&2
echo "codebase: $failures lint rule(s) failed" >&2
for failed in "${FAILED[@]}"; do
echo " - $failed" >&2
done
exit 1
fi

echo "codebase: all ${#RULES[@]} lint rule(s) passed"
68 changes: 23 additions & 45 deletions .mise/tasks/lint/github-actions
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

set -euo pipefail

# shellcheck source=../../../lib/shell-files.sh
source "$MISE_CONFIG_ROOT/lib/shell-files.sh"
# shellcheck source=../../../lib/codebase-config.sh
source "$MISE_CONFIG_ROOT/lib/codebase-config.sh"

FIX="${usage_fix:-false}"

Expand All @@ -27,25 +27,6 @@ if ! command -v actionlint >/dev/null 2>&1; then
exit 127
fi

configured_codebase_lints() {
local toml="$1"

[[ -f "$toml" ]] || return 0

awk '
/^\[_\.codebase\]/ { in_section=1; next }
/^\[/ { in_section=0 }
in_section && /^lint[ \t]*=/ {
gsub(/.*=[ \t]*\[/, "")
gsub(/\].*/, "")
gsub(/"/, "")
gsub(/,/, " ")
gsub(/^[ \t]+|[ \t]+$/, "")
print
}
' "$toml"
}

has_test_task() {
local target="$1"
[[ -f "$target/.mise/tasks/test" || -f "$target/.mise/tasks/test/_default" ]]
Expand Down Expand Up @@ -92,6 +73,7 @@ ensure_codebase_tooling() {
local target="$1"
local toml="$target/mise.toml"
local added=()
local tmp

[[ -f "$toml" ]] || return 0

Expand All @@ -103,6 +85,24 @@ ensure_codebase_tooling() {
if ! section_has_key "$toml" "tools" '^[[:space:]]*"shiv:codebase"[[:space:]]*='; then
ensure_section_key "$toml" "tools" '"shiv:codebase" = "latest"'
added+=("shiv:codebase tool")
elif ! awk '
/^\[tools\][[:space:]]*$/ { in_tools=1; next }
/^\[/ { in_tools=0 }
in_tools && /^[[:space:]]*"shiv:codebase"[[:space:]]*=/ && $0 ~ /"latest"/ { found=1 }
END { exit found ? 0 : 1 }
' "$toml"; then
tmp=$(mktemp)
awk '
/^\[tools\][[:space:]]*$/ { in_tools=1; print; next }
/^\[/ { in_tools=0 }
in_tools && /^[[:space:]]*"shiv:codebase"[[:space:]]*=/ {
print "\"shiv:codebase\" = \"latest\""
next
}
{ print }
' "$toml" > "$tmp"
mv "$tmp" "$toml"
added+=("shiv:codebase tool -> latest")
fi

if [[ ${#added[@]} -gt 0 ]]; then
Expand All @@ -123,7 +123,7 @@ write_default_workflow() {
local target="$1"
local workflow="$target/.github/workflows/test.yml"
local lints
lints=$(configured_codebase_lints "$target/mise.toml")
lints=$(codebase_configured_lint_rules "$target")

if ! has_test_task "$target" && [[ -z "$lints" ]]; then
echo "FAIL $(basename "$target"): no workflow found and no checks inferred (add .mise/tasks/test or [_.codebase].lint)"
Expand Down Expand Up @@ -180,29 +180,7 @@ YAML
cat >> "$workflow" <<'YAML'

- name: Run codebase lints
run: |
for target in \
YAML

# shellcheck disable=SC2086 # intentional word splitting: lints are simple rule names from TOML
set -- $lints
local i=1
local count=$#
local rule
for rule in "$@"; do
if [[ $i -lt $count ]]; then
printf ' lint:%s \\\n' "$rule" >> "$workflow"
else
printf ' lint:%s
' "$rule" >> "$workflow"
fi
i=$((i + 1))
done

cat >> "$workflow" <<'YAML'
do
codebase "$target" "$PWD"
done
run: codebase lint "$PWD"
YAML
fi

Expand Down
131 changes: 18 additions & 113 deletions .mise/tasks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

set -euo pipefail

# shellcheck source=../../lib/codebase-config.sh
source "$MISE_CONFIG_ROOT/lib/codebase-config.sh"

REVERT="${usage_revert:-false}"
CHECK="${usage_check:-false}"

# Find repo root — use CODEBASE_CALLER_PWD (set by shiv) if available,
# otherwise fall back to legacy CALLER_PWD during migration and then current directory.
CALLER="${CODEBASE_CALLER_PWD:-${CALLER_PWD:-$(pwd)}}"
if ! REPO_ROOT=$(git -C "$CALLER" rev-parse --show-toplevel 2>/dev/null); then
if ! REPO_ROOT=$(codebase_git_root "$CALLER"); then
REPO_ROOT=""
fi
if [[ -z "$REPO_ROOT" ]]; then
Expand Down Expand Up @@ -45,136 +48,38 @@ if [[ "$REVERT" == "true" ]]; then
exit 0
fi

# --- Read config ---
# --- Validate config ---
if [[ ! -f "$TOML" ]]; then
[[ "$CHECK" == "true" ]] && exit 1
echo "ERROR: no mise.toml found" >&2
exit 1
fi

RULES=$(awk '
/^\[_\.codebase\]/ { in_section=1; next }
/^\[/ { in_section=0 }
in_section && /^lint[ \t]*=/ {
gsub(/.*=[ \t]*\[/, "")
gsub(/\].*/, "")
gsub(/"/, "")
gsub(/,/, " ")
gsub(/^[ \t]+|[ \t]+$/, "")
print
}
' "$TOML")

if [[ -z "$RULES" ]]; then
if [[ "$CHECK" == "true" ]]; then
exit 1
fi
RULES=()
while IFS= read -r rule; do
[[ -n "$rule" ]] && RULES+=("$rule")
done < <(codebase_configured_lint_rules "$REPO_ROOT")

if [[ ${#RULES[@]} -eq 0 ]]; then
[[ "$CHECK" == "true" ]] && exit 1
echo "ERROR: no lint rules configured in $TOML" >&2
echo "Add to mise.toml:" >&2
echo ' [_.codebase]' >&2
echo ' lint = ["mise-settings", "gum-table"]' >&2
exit 1
fi

# Read per-rule scope overrides from [_.codebase.scope].
# Stored as 'rule<TAB>scope' lines (bash 3 compatible — no associative arrays).
# Tab separator chosen because it can't appear in rule names or fs paths
# read from TOML; ':' would truncate scopes with colons (legal in paths).
USER_SCOPES=""
while IFS='=' read -r rule scope; do
[[ -z "$rule" ]] && continue
rule=$(echo "$rule" | tr -d ' "')
scope=$(echo "$scope" | tr -d ' "')
USER_SCOPES+="${rule}"$'\t'"${scope}"$'\n'
done < <(awk '
/^\[_\.codebase\.scope\]/ { in_section=1; next }
/^\[/ { in_section=0 }
in_section && /=/ { print }
' "$TOML")

# Default scopes per rule (same 'rule<TAB>scope' format).
DEFAULT_SCOPES=$'mise-settings\t.\ngum-table\t.mise/tasks\n'

lookup_scope() {
local map="$1" rule="$2"
printf '%s' "$map" | awk -F'\t' -v r="$rule" '$1==r { print $2; exit }'
}

resolve_scope() {
local rule="$1" value
value=$(lookup_scope "$USER_SCOPES" "$rule")
[[ -n "$value" ]] && { echo "$value"; return; }
value=$(lookup_scope "$DEFAULT_SCOPES" "$rule")
[[ -n "$value" ]] && { echo "$value"; return; }
echo "."
}

# --- Build scope case statement (baked at install time, bash 3 compatible) ---
# Guard against scope strings with single quotes, which would break the
# case body's quoting. Scopes are paths read from mise.toml; apostrophes
# are vanishingly rare but easy to detect and reject cleanly.
SCOPE_CASE=""
for rule in $RULES; do
scope=$(resolve_scope "$rule")
if [[ "$scope" == *"'"* ]]; then
echo "ERROR: scope for rule '$rule' contains a single quote: $scope" >&2
echo " Single quotes in scopes break the generated hook. Remove or escape." >&2
exit 1
fi
SCOPE_CASE+=" $rule) scope='$scope' ;;"$'\n'
done

# --- Generate hook content ---
# The template contains a case statement with ';;'. Bash's parser chokes
# on that inside command-substituted heredocs (even quoted), so we stage
# the template to a tempfile instead of '$(cat <<HOOK ... HOOK)'.
TMP_TEMPLATE=$(mktemp)
trap 'rm -f "$TMP_TEMPLATE"' EXIT
cat > "$TMP_TEMPLATE" <<'HOOK'
NEW_HOOK=$(cat <<'HOOK'
#!/usr/bin/env bash
# Auto-generated by: codebase pre-commit
# Runs codebase lint rules configured in mise.toml [_.codebase].lint
set -eo pipefail
# Delegates to the aggregate lint entrypoint configured in mise.toml.
set -euo pipefail

REPO_ROOT="$(git rev-parse --show-toplevel)"

RULES=(__RULES__)

failures=0

for rule in "${RULES[@]}"; do
scope='.'
case "$rule" in
__SCOPE_CASE__
*) scope='.' ;;
esac

target="$REPO_ROOT/$scope"
[[ "$scope" == "." ]] && target="$REPO_ROOT"

output=$(codebase lint:"$rule" "$target" 2>&1) || status=$?
if [ "${status:-0}" -ne 0 ]; then
echo "codebase: lint:$rule failed" >&2
if filtered=$(echo "$output" | grep -vE '^(INFO|OK)|\[lint:'); then
echo "$filtered" >&2
fi
failures=$((failures + 1))
fi
done

if [ "$failures" -gt 0 ]; then
echo "" >&2
echo "codebase: $failures lint rule(s) failed. Fix issues or commit with --no-verify." >&2
exit 1
fi
exec codebase lint "$REPO_ROOT"
HOOK

# Splice dynamic pieces via bash parameter expansion (handles multi-line
# SCOPE_CASE cleanly; awk -v can't accept embedded newlines).
# Strip trailing newline from SCOPE_CASE to avoid a blank line before '*)'.
SCOPE_CASE="${SCOPE_CASE%$'\n'}"
NEW_HOOK=$(cat "$TMP_TEMPLATE")
NEW_HOOK="${NEW_HOOK//__RULES__/$RULES}"
NEW_HOOK="${NEW_HOOK//__SCOPE_CASE__/$SCOPE_CASE}"
)

# --- Check mode ---
if [[ "$CHECK" == "true" ]]; then
Expand Down
Loading