From fc2759e1fab396d17c18683472d2d0a85e124c86 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Mon, 22 Jun 2026 14:57:52 -0300 Subject: [PATCH 1/2] feat: add ci-lint-enforcement rule to require configured codebase lints in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #49 — a new lint rule that detects repos with [_.codebase].lint configured but no aggregate codebase lint enforcement in CI workflows. Key features: - Scans workflow YAMLs for direct patterns (codebase lint "$PWD"), mise exec indirection, and indirect delegation via local tasks - Warns on drift-prone hard-coded per-rule loops - --fix mode adds aggregate enforcement step and provisions codebase CLI tooling - Respects codebase:ignore mechanism - 13 BATS tests across 7 fixture scenarios (direct, indirect, missing, hardcoded-loop, loop-only, no-config, ignored, no-workflows) All 232 tests pass (13 ci-lint-enforcement tests at indices 45-57). --- .mise/tasks/lint/ci-lint-enforcement | 386 ++++++++++++++++++ .../ci-lint-enforcement.bats | 143 +++++++ .../.github/workflows/test.yml | 14 + .../fixtures/enforced-direct/mise.toml | 5 + .../.github/workflows/test.yml | 10 + .../enforced-indirect/.mise/tasks/test | 4 + .../fixtures/enforced-indirect/mise.toml | 5 + .../hardcoded-loop/.github/workflows/test.yml | 16 + .../fixtures/hardcoded-loop/mise.toml | 5 + .../fixtures/ignored/mise.toml | 7 + .../loop-only/.github/workflows/test.yml | 14 + .../fixtures/loop-only/mise.toml | 5 + .../.github/workflows/test.yml | 10 + .../fixtures/missing-enforcement/mise.toml | 5 + .../fixtures/no-config/mise.toml | 2 + .../fixtures/no-workflows/mise.toml | 5 + 16 files changed, 636 insertions(+) create mode 100755 .mise/tasks/lint/ci-lint-enforcement create mode 100644 test/lint/ci-lint-enforcement/ci-lint-enforcement.bats create mode 100644 test/lint/ci-lint-enforcement/fixtures/enforced-direct/.github/workflows/test.yml create mode 100644 test/lint/ci-lint-enforcement/fixtures/enforced-direct/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.github/workflows/test.yml create mode 100644 test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.mise/tasks/test create mode 100644 test/lint/ci-lint-enforcement/fixtures/enforced-indirect/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/.github/workflows/test.yml create mode 100644 test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/ignored/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/loop-only/.github/workflows/test.yml create mode 100644 test/lint/ci-lint-enforcement/fixtures/loop-only/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/missing-enforcement/.github/workflows/test.yml create mode 100644 test/lint/ci-lint-enforcement/fixtures/missing-enforcement/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/no-config/mise.toml create mode 100644 test/lint/ci-lint-enforcement/fixtures/no-workflows/mise.toml diff --git a/.mise/tasks/lint/ci-lint-enforcement b/.mise/tasks/lint/ci-lint-enforcement new file mode 100755 index 0000000..e41c911 --- /dev/null +++ b/.mise/tasks/lint/ci-lint-enforcement @@ -0,0 +1,386 @@ +#!/usr/bin/env bash +#MISE description="Require configured codebase lints to run in CI" +#USAGE arg "…" help="Paths to codebases to check (one or more)" +#USAGE flag "--fix" help="Add aggregate codebase lint enforcement step to existing workflow" +#USAGE example "codebase lint:ci-lint-enforcement ." header="Check repos enforce their lint portfolio in CI" +#USAGE example "codebase lint:ci-lint-enforcement --fix ." header="Add aggregate lint enforcement to primary workflow" + +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}" + +IFS=' ' read -ra TARGETS <<< "${usage_targets}" + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "ERROR: at least one target is required" >&2 + exit 1 +fi + +for i in "${!TARGETS[@]}"; do + TARGETS[i]=$(resolve_target "${TARGETS[i]}") +done + +# ── detection patterns ────────────────────────────────────────────────────── + +# Direct aggregate enforcement: codebase lint invoked directly in a workflow step +DIRECT_PATTERNS=( + 'codebase lint "$PWD"' + 'codebase lint .' + 'codebase lint \$PWD' +) + +# Mise exec indirection: codebase lint invoked via mise exec +EXEC_PATTERNS=( + 'mise exec.*codebase lint' +) + +# Local task delegation: workflows that run a local test/CI task that may +# itself invoke codebase lint. We trace into the task file to confirm. +TEST_TASK_PATTERNS=( + 'mise run test' + 'mise run ci' + 'mise run check' +) + +# Hard-coded per-rule loop pattern (e.g. codebase lint:mise-settings "$PWD") +# These bypass the configured aggregate and are drift-prone. +PER_RULE_PATTERN='codebase lint:[a-zA-Z0-9_-]+' + +# ── helpers ───────────────────────────────────────────────────────────────── + +collect_workflows() { + local target="$1" + local workflows_dir="$target/.github/workflows" + [[ -d "$workflows_dir" ]] || return 0 + find "$workflows_dir" -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | LC_ALL=C sort +} + +# Strip leading whitespace from a string. +strip_leading_space() { + printf '%s' "$1" | sed 's/^[[:space:]]*//' +} + +# Check if a trimmed YAML line contains an aggregate enforcement pattern. +line_has_aggregate_lint() { + local trimmed="$1" + local pattern + + for pattern in "${DIRECT_PATTERNS[@]}"; do + if [[ "$trimmed" == *"$pattern"* ]]; then + return 0 + fi + done + + for pattern in "${EXEC_PATTERNS[@]}"; do + if [[ "$trimmed" =~ $pattern ]]; then + return 0 + fi + done + + return 1 +} + +# Discover aggregate enforcement by scanning workflow YAML lines. +workflow_has_aggregate_lint() { + local workflow="$1" + local trimmed + + while IFS= read -r line || [[ -n "$line" ]]; do + trimmed=$(strip_leading_space "$line") + [[ -z "$trimmed" || "$trimmed" == '#'* ]] && continue + if line_has_aggregate_lint "$trimmed"; then + return 0 + fi + done < "$workflow" + + return 1 +} + +# Check if a workflow runs a local task that delegates to codebase lint. +workflow_runs_lint_delegating_task() { + local workflow="$1" + local target="$2" + local trimmed task_name + + while IFS= read -r line || [[ -n "$line" ]]; do + trimmed=$(strip_leading_space "$line") + [[ -z "$trimmed" || "$trimmed" == '#'* ]] && continue + + for pattern in "${TEST_TASK_PATTERNS[@]}"; do + if [[ "$trimmed" == *"$pattern"* ]]; then + # Extract the task name from 'mise run ' + task_name=$(printf '%s' "$trimmed" | sed -n 's/.*mise run \([a-zA-Z0-9_-]\+\).*/\1/p') + [[ -z "$task_name" ]] && continue + + if task_invokes_codebase_lint "$target" "$task_name"; then + return 0 + fi + fi + done + done < "$workflow" + + return 1 +} + + +# Check if a local mise task file invokes codebase lint. +task_invokes_codebase_lint() { + local target="$1" + local task_name="$2" + local candidate + + for candidate in "$target/.mise/tasks/$task_name" "$target/.mise/tasks/$task_name/_default"; do + if [[ -f "$candidate" ]] && grep -qE 'codebase lint|CODEBASE_BIN.*lint' "$candidate" 2>/dev/null; then + return 0 + fi + done + + return 1 +} + +# Check if any workflow uses hard-coded per-rule loops. +workflow_has_per_rule_loop() { + local workflow="$1" + [[ $(grep -cE "$PER_RULE_PATTERN" "$workflow" 2>/dev/null || true) -gt 0 ]] +} + +# Count the number of per-rule invocations across all workflows. +count_per_rule_loops() { + local workflows=("$@") + local total=0 wf + for wf in "${workflows[@]}"; do + total=$((total + $(grep -cE "$PER_RULE_PATTERN" "$wf" 2>/dev/null || true))) + done + printf '%s' "$total" +} + +# ── --fix helpers ─────────────────────────────────────────────────────────── + +ensure_codebase_tooling() { + local target="$1" + local toml="$target/mise.toml" + local added=() + local tmp + + [[ -f "$toml" ]] || return 0 + + # Ensure [plugins] section has shiv plugin + if ! grep -q '^shiv = "https://github.com/KnickKnackLabs/vfox-shiv"' "$toml" 2>/dev/null; then + if grep -q '^\[plugins\]' "$toml" 2>/dev/null; then + tmp=$(mktemp) + awk '/^\[plugins\]/ { print; if (!printed) { print "shiv = \"https://github.com/KnickKnackLabs/vfox-shiv\""; printed=1; next } } 1' "$toml" > "$tmp" + mv "$tmp" "$toml" + else + printf '\n[plugins]\nshiv = "https://github.com/KnickKnackLabs/vfox-shiv"\n' >> "$toml" + fi + added+=("shiv plugin") + fi + + # Ensure [tools] section has shiv:codebase + if ! grep -q '"shiv:codebase"' "$toml" 2>/dev/null; then + if grep -q '^\[tools\]' "$toml" 2>/dev/null; then + tmp=$(mktemp) + awk '/^\[tools\]/ { print; if (!printed) { print "\"shiv:codebase\" = \"latest\""; printed=1; next } } 1' "$toml" > "$tmp" + mv "$tmp" "$toml" + else + printf '\n[tools]\n"shiv:codebase" = "latest"\n' >> "$toml" + fi + added+=("shiv:codebase tool") + fi + + if [[ ${#added[@]} -gt 0 ]]; then + echo "FIX $(basename "$target"): provisioned ${added[*]}" + fi +} + +# Add a standard aggregate lint step to the primary workflow. +add_lint_step_to_workflow() { + local target="$1" + local workflow="$2" + local tmp existing_has_lint + + # Skip if the workflow already has aggregate enforcement in any job. + existing_has_lint=false + while IFS= read -r line; do + trimmed=$(strip_leading_space "$line") + if line_has_aggregate_lint "$trimmed"; then + existing_has_lint=true + break + fi + done < "$workflow" + + if $existing_has_lint; then + return 0 + fi + + # Find the last steps: block and insert before its last step. + tmp=$(mktemp) + awk ' + BEGIN { in_steps = 0; last_step = ""; inserted = 0 } + + # Detect start of a steps: block + /^[[:space:]]*steps:[[:space:]]*$/ { + in_steps = 1 + last_step = "" + print + next + } + + # Track the last step entry inside a steps block + in_steps && /^[[:space:]]*- name:/ { + if (last_step != "") print last_step + last_step = $0 + next + } + + in_steps && /^[[:space:]]*- run:/ { + if (last_step != "") print last_step + last_step = $0 + next + } + + # Outdent back to a non-step key ends the steps block + in_steps && /^[a-zA-Z]/ && !/^[[:space:]]*- / && last_step != "" { + # Insert our step before the last tracked step + print " - name: Run codebase lints" + print " run: codebase lint \"$PWD\"" + inserted = 1 + print last_step + last_step = "" + in_steps = 0 + print + next + } + + # End of file: flush last step and insert + { + print + } + + END { + if (!inserted && last_step != "") { + print " - name: Run codebase lints" + print " run: codebase lint \"$PWD\"" + print last_step + } + } + ' "$workflow" > "$tmp" + mv "$tmp" "$workflow" + + echo "FIXED $(basename "$target"): added aggregate codebase lint step to $(basename "$workflow")" +} + +# ── main ──────────────────────────────────────────────────────────────────── + +failures=0 +FIXED_REPOS=() + +for target in "${TARGETS[@]}"; do + if [[ ! -e "$target" ]]; then + echo "ERROR: target does not exist: $target" >&2 + exit 1 + fi + + name=$(basename "$target") + toml="$target/mise.toml" + + # File-level ignore + if [[ -f "$toml" ]] && grep -m1 -q 'codebase:ignore ci-lint-enforcement' "$toml" 2>/dev/null; then + echo "SKIP $name (codebase:ignore)" + continue + fi + + # Read configured lint rules + RULES=() + while IFS= read -r rule; do + [[ -n "$rule" ]] && RULES+=("$rule") + done < <(codebase_configured_lint_rules "$target") + + # No lint config → nothing to enforce + if [[ ${#RULES[@]} -eq 0 ]]; then + echo "SKIP $name (no [_.codebase].lint configured)" + continue + fi + + # Collect workflows + workflows=() + while IFS= read -r wf; do + [[ -n "$wf" ]] && workflows+=("$wf") + done < <(collect_workflows "$target") + + # No workflows at all → enforcement not possible + if [[ ${#workflows[@]} -eq 0 ]]; then + echo "FAIL $name: lint configured but no CI workflows found" + echo " hint: run 'codebase lint:github-actions --fix $target' to create a default workflow" + failures=$((failures + 1)) + continue + fi + + # Check enforcement across all workflows + found_enforcement=false + found_per_rule_loop=false + + for wf in "${workflows[@]}"; do + if workflow_has_aggregate_lint "$wf"; then + found_enforcement=true + fi + + # Check indirect delegation (mise run test → tasks/test → codebase lint) + if workflow_runs_lint_delegating_task "$wf" "$target"; then + found_enforcement=true + fi + + # Check for hard-coded per-rule loops (warn, not fail) + if workflow_has_per_rule_loop "$wf"; then + found_per_rule_loop=true + fi + done + + if $found_enforcement; then + if $found_per_rule_loop; then + loop_count=$(count_per_rule_loops "${workflows[@]}") + echo "WARN $name: aggregate lint enforced but $loop_count per-rule invocation(s) detected (drift-prone)" + else + echo "OK $name (${#RULES[@]} rule(s) enforced in ${#workflows[@]} workflow(s))" + fi + continue + fi + + # ── --fix mode ──────────────────────────────────────────────────────── + if [[ "$FIX" == "true" ]]; then + # Provision codebase tooling if missing + ensure_codebase_tooling "$target" + + # Add enforcement step to the primary workflow + primary="${workflows[0]}" + add_lint_step_to_workflow "$target" "$primary" + + if $found_per_rule_loop; then + loop_count=$(count_per_rule_loops "${workflows[@]}") + echo "WARN $name: aggregate step added but $loop_count per-rule loop(s) remain (review manually)" + fi + + FIXED_REPOS+=("$name") + continue + fi + + # ── fail ────────────────────────────────────────────────────────────── + echo "FAIL $name: lint configured but no aggregate enforcement in CI" + if $found_per_rule_loop; then + loop_count=$(count_per_rule_loops "${workflows[@]}") + echo " detail: $loop_count hard-coded per-rule invocation(s) found — these bypass the configured aggregate" + fi + echo " hint: add 'run: codebase lint \"\$PWD\"' to your CI workflow, or run with --fix" + failures=$((failures + 1)) +done + +if [[ ${#FIXED_REPOS[@]} -gt 0 ]]; then + echo "FIXED: ${FIXED_REPOS[*]}" +fi + +exit "$failures" \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats b/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats new file mode 100644 index 0000000..64643e1 --- /dev/null +++ b/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats @@ -0,0 +1,143 @@ +#!/usr/bin/env bats +# Tests for lint:ci-lint-enforcement rule + +load ../../test_helper + +setup() { + FIXTURES="$BATS_TEST_DIRNAME/fixtures" +} + +copy_fixture() { + local name="$1" + local dest="$BATS_TEST_TMPDIR/$name" + cp -R "$FIXTURES/$name" "$dest" + echo "$dest" +} + +# ── direct enforcement ────────────────────────────────────────────────────── + +@test "lint: passes when aggregate codebase lint is in workflow" { + run codebase lint:ci-lint-enforcement "$FIXTURES/enforced-direct" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"enforced-direct"* ]] + [[ "$output" == *"2 rule(s)"* ]] +} + +# ── indirect enforcement via local task ───────────────────────────────────── + +@test "lint: passes when aggregate lint runs through local task delegation" { + run codebase lint:ci-lint-enforcement "$FIXTURES/enforced-indirect" + [ "$status" -eq 0 ] + [[ "$output" == *"OK"*"enforced-indirect"* ]] + [[ "$output" == *"3 rule(s)"* ]] +} + +# ── missing enforcement ───────────────────────────────────────────────────── + +@test "lint: fails when lint configured but not enforced in CI" { + run codebase lint:ci-lint-enforcement "$FIXTURES/missing-enforcement" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"missing-enforcement"* ]] + [[ "$output" == *"no aggregate enforcement in CI"* ]] +} + +# ── hard-coded per-rule loop ──────────────────────────────────────────────── + +@test "lint: warns on hard-coded per-rule loops" { + run codebase lint:ci-lint-enforcement "$FIXTURES/hardcoded-loop" + [ "$status" -eq 0 ] + [[ "$output" == *"WARN"*"hardcoded-loop"* ]] + [[ "$output" == *"per-rule invocation"* ]] + [[ "$output" == *"3 per-rule invocation(s)"* ]] +} + +# ── no lint config → skip ─────────────────────────────────────────────────── + +@test "lint: skips when no [_.codebase].lint is configured" { + run codebase lint:ci-lint-enforcement "$FIXTURES/no-config" + [ "$status" -eq 0 ] + [[ "$output" == *"SKIP"*"no-config"* ]] + [[ "$output" == *"no [_.codebase].lint configured"* ]] +} + +# ── codebase:ignore ──────────────────────────────────────────────────────── + +@test "lint: skips when codebase:ignore ci-lint-enforcement is set" { + run codebase lint:ci-lint-enforcement "$FIXTURES/ignored" + [ "$status" -eq 0 ] + [[ "$output" == *"SKIP"*"ignored"* ]] +} + +# ── --fix mode ────────────────────────────────────────────────────────────── + +@test "fix: adds aggregate lint step to workflow when missing" { + target=$(copy_fixture missing-enforcement) + + run codebase lint:ci-lint-enforcement --fix "$target" + [ "$status" -eq 0 ] + [[ "$output" == *"FIXED"*"missing-enforcement"* ]] + + workflow="$target/.github/workflows/test.yml" + [ -f "$workflow" ] + grep -q 'Run codebase lints' "$workflow" + grep -q 'codebase lint "\$PWD"' "$workflow" +} + +@test "fix: provisions codebase CLI tooling when missing" { + target=$(copy_fixture missing-enforcement) + + run codebase lint:ci-lint-enforcement --fix "$target" + [ "$status" -eq 0 ] + [[ "$output" == *"provisioned"* ]] + + grep -q 'shiv = "https://github.com/KnickKnackLabs/vfox-shiv"' "$target/mise.toml" + grep -q '"shiv:codebase"' "$target/mise.toml" +} + +@test "fix: does not duplicate existing provisioning" { + target=$(copy_fixture enforced-direct) + + run codebase lint:ci-lint-enforcement --fix "$target" + [ "$status" -eq 0 ] + + # Should not change the workflow since it already has enforcement + grep -q 'Run codebase lints' "$target/.github/workflows/test.yml" + [ "$(grep -c 'Run codebase lints' "$target/.github/workflows/test.yml")" -eq 1 ] +} + +@test "fix: warns about remaining per-rule loops" { + target=$(copy_fixture loop-only) + + run codebase lint:ci-lint-enforcement --fix "$target" + [ "$status" -eq 0 ] + [[ "$output" == *"FIXED"*"loop-only"* ]] + [[ "$output" == *"WARN"*"loop-only"* ]] + [[ "$output" == *"3 per-rule loop(s) remain"* ]] + + # Should still have added aggregate step + grep -q 'Run codebase lints' "$target/.github/workflows/test.yml" +} + +# ── edge cases ────────────────────────────────────────────────────────────── + +@test "lint: fails when lint configured but no workflows exist" { + run codebase lint:ci-lint-enforcement "$FIXTURES/no-workflows" + [ "$status" -ne 0 ] + [[ "$output" == *"FAIL"*"no-workflows"* ]] + [[ "$output" == *"no CI workflows found"* ]] +} + +@test "lint: passes multiple targets — mixed pass and fail" { + run codebase lint:ci-lint-enforcement \ + "$FIXTURES/enforced-direct" \ + "$FIXTURES/missing-enforcement" + [ "$status" -ne 0 ] + [[ "$output" == *"OK"*"enforced-direct"* ]] + [[ "$output" == *"FAIL"*"missing-enforcement"* ]] +} + +@test "lint: fails when target does not exist" { + run codebase lint:ci-lint-enforcement "$FIXTURES/does-not-exist" + [ "$status" -ne 0 ] + [[ "$output" == *"does not exist"* ]] +} \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/enforced-direct/.github/workflows/test.yml b/test/lint/ci-lint-enforcement/fixtures/enforced-direct/.github/workflows/test.yml new file mode 100644 index 0000000..f1f6d5c --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/enforced-direct/.github/workflows/test.yml @@ -0,0 +1,14 @@ +name: Test +on: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up mise + uses: jdx/mise-action@v4 + - name: Run codebase lints + run: codebase lint "$PWD" + - name: Run tests + run: mise run test diff --git a/test/lint/ci-lint-enforcement/fixtures/enforced-direct/mise.toml b/test/lint/ci-lint-enforcement/fixtures/enforced-direct/mise.toml new file mode 100644 index 0000000..146b5c1 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/enforced-direct/mise.toml @@ -0,0 +1,5 @@ +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings", "shellcheck"] \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.github/workflows/test.yml b/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.github/workflows/test.yml new file mode 100644 index 0000000..c7e06d6 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.github/workflows/test.yml @@ -0,0 +1,10 @@ +name: Test +on: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Run tests + run: mise run test diff --git a/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.mise/tasks/test b/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.mise/tasks/test new file mode 100644 index 0000000..e4b8c61 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/.mise/tasks/test @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +"$CODEBASE_BIN" lint "$PWD" +# ... run other tests \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/mise.toml b/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/mise.toml new file mode 100644 index 0000000..590cd17 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/enforced-indirect/mise.toml @@ -0,0 +1,5 @@ +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings", "gum-table", "shellcheck"] \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/.github/workflows/test.yml b/test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/.github/workflows/test.yml new file mode 100644 index 0000000..3760890 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: Test +on: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Run aggregate codebase lints + run: codebase lint "$PWD" + - name: Lint mise-settings + run: codebase lint:mise-settings "$PWD" + - name: Lint shellcheck + run: codebase lint:shellcheck "$PWD" + - name: Lint gum-table + run: codebase lint:gum-table "$PWD" diff --git a/test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/mise.toml b/test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/mise.toml new file mode 100644 index 0000000..603eb6c --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/hardcoded-loop/mise.toml @@ -0,0 +1,5 @@ +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings", "shellcheck", "gum-table"] \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/ignored/mise.toml b/test/lint/ci-lint-enforcement/fixtures/ignored/mise.toml new file mode 100644 index 0000000..22d70db --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/ignored/mise.toml @@ -0,0 +1,7 @@ +# codebase:ignore ci-lint-enforcement — repo handles lint differently + +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings"] \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/loop-only/.github/workflows/test.yml b/test/lint/ci-lint-enforcement/fixtures/loop-only/.github/workflows/test.yml new file mode 100644 index 0000000..192b849 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/loop-only/.github/workflows/test.yml @@ -0,0 +1,14 @@ +name: Test +on: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Lint mise-settings + run: codebase lint:mise-settings "$PWD" + - name: Lint shellcheck + run: codebase lint:shellcheck "$PWD" + - name: Lint gum-table + run: codebase lint:gum-table "$PWD" \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/loop-only/mise.toml b/test/lint/ci-lint-enforcement/fixtures/loop-only/mise.toml new file mode 100644 index 0000000..603eb6c --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/loop-only/mise.toml @@ -0,0 +1,5 @@ +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings", "shellcheck", "gum-table"] \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/missing-enforcement/.github/workflows/test.yml b/test/lint/ci-lint-enforcement/fixtures/missing-enforcement/.github/workflows/test.yml new file mode 100644 index 0000000..c7e06d6 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/missing-enforcement/.github/workflows/test.yml @@ -0,0 +1,10 @@ +name: Test +on: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Run tests + run: mise run test diff --git a/test/lint/ci-lint-enforcement/fixtures/missing-enforcement/mise.toml b/test/lint/ci-lint-enforcement/fixtures/missing-enforcement/mise.toml new file mode 100644 index 0000000..146b5c1 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/missing-enforcement/mise.toml @@ -0,0 +1,5 @@ +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings", "shellcheck"] \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/no-config/mise.toml b/test/lint/ci-lint-enforcement/fixtures/no-config/mise.toml new file mode 100644 index 0000000..066be7e --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/no-config/mise.toml @@ -0,0 +1,2 @@ +[settings] +quiet = true \ No newline at end of file diff --git a/test/lint/ci-lint-enforcement/fixtures/no-workflows/mise.toml b/test/lint/ci-lint-enforcement/fixtures/no-workflows/mise.toml new file mode 100644 index 0000000..146b5c1 --- /dev/null +++ b/test/lint/ci-lint-enforcement/fixtures/no-workflows/mise.toml @@ -0,0 +1,5 @@ +[settings] +quiet = true + +[_.codebase] +lint = ["mise-settings", "shellcheck"] \ No newline at end of file From 3cb3f880535d925b07f48c911cf417608b2eec30 Mon Sep 17 00:00:00 2001 From: olavostauros Date: Thu, 25 Jun 2026 15:46:15 -0300 Subject: [PATCH 2/2] style: replace decorative rulers with single-line headers --- .mise/tasks/lint/ci-lint-enforcement | 12 ++++++------ .../ci-lint-enforcement/ci-lint-enforcement.bats | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.mise/tasks/lint/ci-lint-enforcement b/.mise/tasks/lint/ci-lint-enforcement index e41c911..d93fe59 100755 --- a/.mise/tasks/lint/ci-lint-enforcement +++ b/.mise/tasks/lint/ci-lint-enforcement @@ -25,7 +25,7 @@ for i in "${!TARGETS[@]}"; do TARGETS[i]=$(resolve_target "${TARGETS[i]}") done -# ── detection patterns ────────────────────────────────────────────────────── +# === detection patterns === # Direct aggregate enforcement: codebase lint invoked directly in a workflow step DIRECT_PATTERNS=( @@ -51,7 +51,7 @@ TEST_TASK_PATTERNS=( # These bypass the configured aggregate and are drift-prone. PER_RULE_PATTERN='codebase lint:[a-zA-Z0-9_-]+' -# ── helpers ───────────────────────────────────────────────────────────────── +# === helpers === collect_workflows() { local target="$1" @@ -159,7 +159,7 @@ count_per_rule_loops() { printf '%s' "$total" } -# ── --fix helpers ─────────────────────────────────────────────────────────── +# === --fix helpers === ensure_codebase_tooling() { local target="$1" @@ -275,7 +275,7 @@ add_lint_step_to_workflow() { echo "FIXED $(basename "$target"): added aggregate codebase lint step to $(basename "$workflow")" } -# ── main ──────────────────────────────────────────────────────────────────── +# === main === failures=0 FIXED_REPOS=() @@ -351,7 +351,7 @@ for target in "${TARGETS[@]}"; do continue fi - # ── --fix mode ──────────────────────────────────────────────────────── + # === --fix mode === if [[ "$FIX" == "true" ]]; then # Provision codebase tooling if missing ensure_codebase_tooling "$target" @@ -369,7 +369,7 @@ for target in "${TARGETS[@]}"; do continue fi - # ── fail ────────────────────────────────────────────────────────────── + # === fail === echo "FAIL $name: lint configured but no aggregate enforcement in CI" if $found_per_rule_loop; then loop_count=$(count_per_rule_loops "${workflows[@]}") diff --git a/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats b/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats index 64643e1..0e69ce8 100644 --- a/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats +++ b/test/lint/ci-lint-enforcement/ci-lint-enforcement.bats @@ -14,7 +14,7 @@ copy_fixture() { echo "$dest" } -# ── direct enforcement ────────────────────────────────────────────────────── +# === direct enforcement === @test "lint: passes when aggregate codebase lint is in workflow" { run codebase lint:ci-lint-enforcement "$FIXTURES/enforced-direct" @@ -23,7 +23,7 @@ copy_fixture() { [[ "$output" == *"2 rule(s)"* ]] } -# ── indirect enforcement via local task ───────────────────────────────────── +# === indirect enforcement via local task === @test "lint: passes when aggregate lint runs through local task delegation" { run codebase lint:ci-lint-enforcement "$FIXTURES/enforced-indirect" @@ -32,7 +32,7 @@ copy_fixture() { [[ "$output" == *"3 rule(s)"* ]] } -# ── missing enforcement ───────────────────────────────────────────────────── +# === missing enforcement === @test "lint: fails when lint configured but not enforced in CI" { run codebase lint:ci-lint-enforcement "$FIXTURES/missing-enforcement" @@ -41,7 +41,7 @@ copy_fixture() { [[ "$output" == *"no aggregate enforcement in CI"* ]] } -# ── hard-coded per-rule loop ──────────────────────────────────────────────── +# === hard-coded per-rule loop === @test "lint: warns on hard-coded per-rule loops" { run codebase lint:ci-lint-enforcement "$FIXTURES/hardcoded-loop" @@ -51,7 +51,7 @@ copy_fixture() { [[ "$output" == *"3 per-rule invocation(s)"* ]] } -# ── no lint config → skip ─────────────────────────────────────────────────── +# === no lint config → skip === @test "lint: skips when no [_.codebase].lint is configured" { run codebase lint:ci-lint-enforcement "$FIXTURES/no-config" @@ -60,7 +60,7 @@ copy_fixture() { [[ "$output" == *"no [_.codebase].lint configured"* ]] } -# ── codebase:ignore ──────────────────────────────────────────────────────── +# === codebase:ignore === @test "lint: skips when codebase:ignore ci-lint-enforcement is set" { run codebase lint:ci-lint-enforcement "$FIXTURES/ignored" @@ -68,7 +68,7 @@ copy_fixture() { [[ "$output" == *"SKIP"*"ignored"* ]] } -# ── --fix mode ────────────────────────────────────────────────────────────── +# === --fix mode === @test "fix: adds aggregate lint step to workflow when missing" { target=$(copy_fixture missing-enforcement) @@ -118,7 +118,7 @@ copy_fixture() { grep -q 'Run codebase lints' "$target/.github/workflows/test.yml" } -# ── edge cases ────────────────────────────────────────────────────────────── +# === edge cases === @test "lint: fails when lint configured but no workflows exist" { run codebase lint:ci-lint-enforcement "$FIXTURES/no-workflows"