Skip to content

Refactor pre-commit checks for lower verbosity and faster feedback #1769

@josecelano

Description

@josecelano

Goal

Improve local commit-time feedback by making pre-commit output concise by default and reducing unnecessary runtime, while preserving strong quality guarantees through pre-push and CI.

Background

Current pre-commit flow in contrib/dev-tools/git/hooks/pre-commit.sh:

  1. cargo machete
  2. linter all
  3. cargo test --doc --workspace
  4. cargo test --tests --benches --examples --workspace --all-targets --all-features

Current pre-push flow in contrib/dev-tools/git/hooks/pre-push.sh already runs comprehensive validation and includes E2E. CI in .github/workflows/testing.yaml also runs E2E matrix jobs.

Key finding:

  • E2E is not part of pre-commit today. The pre-commit pain is mainly verbosity and broad test scope for frequent local commits.

Automation policy constraint:

  • We do not want to couple workflow automation exclusively to GitHub-native services (for example Dependabot) when defining core maintenance processes.
  • The process should remain portable: executable in different CI/CD infrastructures and usable with different AI providers.
  • GitHub ecosystem tools can still be used as optional integrations, but not as the only execution path.

Scope

In Scope

  • Add concise/verbose output modes to pre-commit with better failure summaries and log-path reporting.
  • Measure current vs proposed pre-commit runtime and output quality.
  • Define and document command ownership by tier (pre-commit, pre-push, CI).
  • Adjust pre-commit step composition to optimize local cycle time without reducing merge safety.

Out of Scope

  • Removing comprehensive checks from pre-push/CI.
  • E2E redesign.
  • Changes unrelated to developer workflow/quality gates.

Deep Analysis Summary

A. Verbosity issues

  • Current commands stream full output, producing noisy terminal sessions.
  • Failures can be hard to spot in long logs.
  • High-volume output contributes to tooling output transport instability for agent execution.

B. Runtime issues

  • Pre-commit runs broad workspace tests on every commit.
  • Heavy checks are duplicated in pre-push/CI.
  • For docs/small changes, local wait time is disproportionate to change risk.

Observed baseline run (2026-05-13, local):

Step Command Elapsed
1 cargo machete 0s
2 linter all 7s
3 cargo test --doc --workspace 50s
4 cargo test --tests --benches --examples --workspace --all-targets --all-features 17s
Total pre-commit script 1m 14s

Observation from this run: documentation tests were slower than the broader test command. Profile decisions must be based on multi-run data, not assumptions.

C. Boundary between pre-commit and heavier tiers

  • Pre-commit should optimize for fast, high-signal local feedback.
  • Pre-push and CI should remain comprehensive and authoritative for merge readiness.

Proposed Changes

Task 1: Add output modes and failure-focused summaries

CLI contract:

  • Add --format=<text|json> where:
    • --format=text is the default (human-friendly terminal output)
    • --format=json emits a single JSON document to stdout
  • Add --verbosity=<concise|verbose> where:
    • --verbosity=concise is the default
    • --verbosity=verbose streams full command output
  • Keep --verbose as a compatibility alias for --verbosity=verbose.
  • Define precedence explicitly:
    • when --format=json, output remains structured JSON regardless of verbosity value
    • for --format=text, verbosity controls concise vs full streaming output
  • Define argument conflict/error behavior explicitly:
    • duplicate --format/--verbosity flags: last value wins
    • --verbose alias sets --verbosity=verbose
    • invalid values (for example --format=xml): fail with exit code 2 and usage hint
    • unknown flags: fail with exit code 2 and usage hint
    • output channel contract: structured output goes to stdout, diagnostics/errors to stderr

Modes matrix:

Format Verbosity Behavior
text concise (default) High-signal summary per step
text verbose Full streaming command output
json concise or verbose Single JSON document to stdout
  • Add --format and --verbosity flags to contrib/dev-tools/git/hooks/pre-commit.sh.
  • In concise mode, capture per-step logs and print only:
    • step name, pass/fail, elapsed time
    • log path and a short failure tail when a step fails
  • Keep full streaming output in --verbosity=verbose mode for --format=text.
  • In --format=json mode, write a single JSON document to stdout (see examples below).

Example command calls

# Default behavior
./contrib/dev-tools/git/hooks/pre-commit.sh

# Explicit text + concise (equivalent to default)
./contrib/dev-tools/git/hooks/pre-commit.sh --format=text --verbosity=concise

# Text + verbose
./contrib/dev-tools/git/hooks/pre-commit.sh --format=text --verbosity=verbose

# Compatibility alias for verbose text output
./contrib/dev-tools/git/hooks/pre-commit.sh --format=text --verbose

# Structured output for agents/scripts
./contrib/dev-tools/git/hooks/pre-commit.sh --format=json

Example: concise default (all pass)

./contrib/dev-tools/git/hooks/pre-commit.sh
Running pre-commit checks...

[Step 1/4] Checking for unused dependencies (cargo machete)  ... PASS  (0s)
[Step 2/4] Running all linters (linter all)                  ... PASS  (7s)
[Step 3/4] Running documentation tests                       ... PASS (50s)
[Step 4/4] Running all tests                                 ... PASS (17s)

==========================================
SUCCESS: All pre-commit checks passed! (1m 14s)
==========================================

Example: concise default (step fails)

./contrib/dev-tools/git/hooks/pre-commit.sh
Running pre-commit checks...

[Step 1/4] Checking for unused dependencies (cargo machete)  ... PASS  (0s)
[Step 2/4] Running all linters (linter all)                  ... FAIL (11s)  log: /tmp/pre-commit-linter-all-20260513-083055.log
    error[E0001]: unused variable `x` at src/lib.rs:42
    error: aborting due to 1 previous error
    (2 lines shown — full log: /tmp/pre-commit-linter-all-20260513-083055.log)

==========================================
FAILED: Pre-commit checks failed!
Fix the errors above before committing.
==========================================

Example: --format=json mode (all pass)

./contrib/dev-tools/git/hooks/pre-commit.sh --format=json
{
  "schema_version": 1,
  "status": "pass",
  "exit_code": 0,
  "elapsed_seconds": 74,
  "steps": [
    {
      "name": "Checking for unused dependencies",
      "command": "cargo machete",
      "status": "pass",
      "elapsed_seconds": 0
    },
    {
      "name": "Running all linters",
      "command": "linter all",
      "status": "pass",
      "elapsed_seconds": 7
    },
    {
      "name": "Running documentation tests",
      "command": "cargo test --doc --workspace",
      "status": "pass",
      "elapsed_seconds": 50
    },
    {
      "name": "Running all tests",
      "command": "cargo test --tests --benches --examples --workspace --all-targets --all-features",
      "status": "pass",
      "elapsed_seconds": 17
    }
  ]
}

Example: --format=json mode (step fails)

./contrib/dev-tools/git/hooks/pre-commit.sh --format=json
{
  "schema_version": 1,
  "status": "fail",
  "exit_code": 1,
  "elapsed_seconds": 11,
  "failed_step": "Running all linters",
  "steps": [
    {
      "name": "Checking for unused dependencies",
      "command": "cargo machete",
      "status": "pass",
      "elapsed_seconds": 0
    },
    {
      "name": "Running all linters",
      "command": "linter all",
      "status": "fail",
      "elapsed_seconds": 11,
      "log_path": "/tmp/pre-commit-linter-all-20260513-083055.log",
      "failure_tail": [
        "error[E0001]: unused variable `x` at src/lib.rs:42",
        "error: aborting due to 1 previous error"
      ]
    }
  ]
}

Task 2: Baseline timing and propose tuned pre-commit profile

  • Measure current pre-commit runtime over at least 3 runs.
  • Measure candidate profile runtime over at least 3 runs.
  • Compare results and choose a profile with documented rationale.

Candidate profiles:

  • Profile A (provisional until multi-run evidence is collected): cargo machete + linter all + cargo test --doc --workspace.
  • Profile B: retain full tests but with concise default output.

Evaluation note:

  • Because a real baseline run showed cargo test --doc --workspace as the slowest step, the final profile selection must be decided after the required multi-run timing table is completed.

Task 3: Clarify check tiers and ownership

  • Document which checks are mandatory at each tier:
    • pre-commit (fast local gate)
    • pre-push (comprehensive developer gate)
    • CI (merge authority)
  • Keep E2E explicitly out of pre-commit and documented as pre-push/CI responsibility.

Task 4: Update workflow docs and skills

Implementation Plan

Status values: TODO, IN_PROGRESS, BLOCKED, DONE.

ID Status Task Notes / Expected Output
T1 TODO Baseline current pre-commit stats Runtime and output-size baseline collected.
T2 TODO Implement output mode refactor Concise default + verbose opt-in implemented.
T3 TODO Select and apply runtime profile Profile selected with measured trade-off rationale.
T4 TODO Update docs/skills Workflow docs and skills aligned.
T5 TODO Validate gates and regression linter all and relevant test checks pass.

Progress Tracking

Workflow Checkpoints

  • Spec drafted in docs/issues/drafts/
  • Spec reviewed and approved by user/maintainer
  • GitHub issue created and issue number added to this spec
  • Implementation completed
  • Reviewer validated acceptance criteria and updated checkboxes
  • Committer verified spec progress is up to date before commit
  • Issue closed and spec moved from docs/issues/open/ to docs/issues/closed/

Progress Log

  • 2026-05-13 07:33 UTC - Copilot - Created focused pre-commit refactor draft split from combined proposal.
  • 2026-05-13 08:42 UTC - Copilot - Executed ./contrib/dev-tools/git/hooks/pre-commit.sh and captured baseline output (1m 14s total; docs 50s, tests 17s).

Acceptance Criteria

  • AC1: Pre-commit supports --format=<text|json> and --verbosity=<concise|verbose> with documented defaults and precedence rules.
  • AC2: --format=text --verbosity=concise prints high-signal step summaries and log paths on failure; --format=json emits a single valid JSON document matching the schema in Task 1.
  • AC2.1: Invalid flags/values fail with exit code 2, print usage guidance, and write diagnostics to stderr.
  • AC3: Chosen pre-commit profile is backed by timing data from multiple runs.
  • AC4: Check-tier ownership is documented and consistent across scripts and docs.
  • AC5: E2E remains excluded from pre-commit and explicitly mapped to pre-push/CI.
  • AC6: linter all exits with code 0 after changes.
  • AC7: Relevant checks pass for modified hook behavior.

Acceptance Verification

AC ID Status (TODO/DONE) Evidence
AC1 TODO Updated pre-commit script usage/flags (--format, --verbosity, --verbose alias)
AC2 TODO Sample --format=text --verbosity=concise logs and --format=json output against schema in Task 1
AC2.1 TODO Negative tests for invalid/unknown flags and stderr/exit-code checks
AC3 TODO Runtime comparison table
AC4 TODO Updated docs/skills references
AC5 TODO Hook/CI command mapping
AC6 TODO linter all output
AC7 TODO Test/check outputs

Risks and Trade-offs

  • Reducing local checks too far can miss early regressions.
    • Mitigation: keep pre-push/CI comprehensive and document boundaries clearly.
  • Concise output can hide details during debugging.
    • Mitigation: preserve full verbose mode and always record log file paths.
  • Hook complexity can grow over time (argument parsing, structured output, log orchestration).
    • Mitigation: if complexity becomes hard to maintain in shell, migrate the hook logic to a small Rust CLI and keep the shell hook as a thin entrypoint.
  • Captured logs can include ANSI color codes and multiline errors that are harder to parse in JSON consumers.
    • Mitigation: strip ANSI sequences in --format=json mode and keep raw logs on disk.
  • Script interruption (Ctrl+C) can leave partial state or truncated output.
    • Mitigation: add trap handling that emits a deterministic non-zero exit and a final status line/JSON payload where feasible.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions