Ctrl K
ComunidadGuías…

lefthook.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Lefthook config (https://lefthook.dev). Run `lefthook install` once, then
# every git commit / push runs the hooks below. Anything that fails here
# blocks the commit / push, so keep the pre-commit phase FAST (sub-second on
# a typical change). Slow checks belong in pre-push or CI.
#
# Bypass (use sparingly, with a reason): `git commit --no-verify`. CI is the
# backstop and will re-run everything.

pre-commit:
  parallel: true
  commands:
    gofmt:
      # Lefthook globs match the whole path (not just basename), so "*.go"
      # would only catch Go files at the repo root. Use a recursive glob to
      # cover server/, agent/, internal/, test/, and anything else.
      #
      # The `glob` directive gates whether the hook RUNS, but `{staged_files}`
      # expands to every staged file in the commit, not just files matching
      # the glob. Pass `{staged_files}` through a Go-suffix filter before
      # handing it to gofmt so a mixed-language commit (e.g. Go + .rego +
      # .md) does not trip gofmt on the non-Go files with `illegal character`
      # errors that abort the hook before it can pass.
      glob: "**/*.go"
      run: |
        go_files=$(printf '%s\n' {staged_files} | grep -E '\.go$' || true)
        if [ -z "$go_files" ]; then exit 0; fi
        # tr '\n' '\0' + xargs -0 reads filenames null-delimited so a
        # filename containing a space (rare but legal) does not get
        # split into two arguments. Portable across BSD + GNU xargs.
        unformatted=$(printf '%s\n' "$go_files" | tr '\n' '\0' | xargs -0 gofmt -s -l)
        if [ -n "$unformatted" ]; then
          echo "gofmt -s would rewrite the following files. Running it now; re-stage and recommit:"
          echo "$unformatted"
          printf '%s\n' "$go_files" | tr '\n' '\0' | xargs -0 gofmt -s -w
          exit 1
        fi
      stage_fixed: true

    eslint:
      # Scope eslint to the UI source tree; running it on everything scans node_modules needlessly.
      glob: "ui/src/**/*.{ts,tsx,js,jsx}"
      root: "ui/"
      run: npx eslint --fix --max-warnings=0 {staged_files}
      stage_fixed: true

    swiftformat:
      # Optional: only runs if swiftformat is installed (`brew install swiftformat`).
      glob: "extension/edr/**/*.swift"
      run: |
        if command -v swiftformat >/dev/null 2>&1; then
          swiftformat {staged_files}
        fi
      stage_fixed: true

    markdownlint:
      # Lint staged markdown via markdownlint-cli2 (config in .markdownlint-cli2.yaml).
      # Uses --fix for the safe rules; what cannot be auto-fixed (MD040 language tags)
      # still surfaces and blocks the commit. Prose wrapping + table style are NOT
      # markdownlint's job here; the prettier-md hook below owns those.
      #
      # npx caches the package after the first download, so steady-state cost is ~1s.
      glob: "**/*.md"
      run: |
        if command -v npx >/dev/null 2>&1; then
          npx -y -p markdownlint-cli2@0.18.1 markdownlint-cli2 --fix {staged_files}
        fi
      stage_fixed: true

    prettier-md:
      # Enforce the no-hard-wrap convention + aligned tables on staged markdown via
      # Prettier (config in .prettierrc.yaml, proseWrap: never; exclusions in
      # .prettierignore). --write reflows in place and stage_fixed re-stages, so a
      # wrapped paragraph is silently collapsed rather than blocking the commit.
      #
      # {staged_files} expands to every staged path regardless of glob, and Prettier
      # errors on files it has no parser for, so filter to .md before invoking (same
      # pattern as the gofmt hook above). --ignore-path keeps .prettierignore honored.
      glob: "**/*.md"
      run: |
        if command -v npx >/dev/null 2>&1; then
          md_files=$(printf '%s\n' {staged_files} | grep -E '\.md$' || true)
          if [ -z "$md_files" ]; then exit 0; fi
          printf '%s\n' "$md_files" | tr '\n' '\0' | xargs -0 npx -y prettier@3.3.3 --ignore-path .prettierignore --write
        fi
      stage_fixed: true

    dash-lint:
      # Mirror the CI "No em dashes" gate (.github/workflows/no-emdash.yml) at commit time, scoped to staged files like
      # every other hook here. tools/lint-no-emdash.sh bans em/en dashes (U+2014 / U+2013); tools/dash-lint bans the
      # spaced ASCII hyphen `' - '` in Markdown prose and in code comments + string literals. This closes the gap that let
      # PR #414's tasks.md em-dashes reach CI: `task lint:dashes` scans `git ls-files`, so the then-untracked new files
      # were invisible to a manual run, and no pre-commit hook covered the rule. {staged_files} expands to every staged
      # path regardless of glob; both tools filter internally (grep -I binary-skip / type-aware), so the raw list is safe.
      # `go run ./tools/dash-lint` compiles once and is cached, so steady-state cost is ~1s alongside the markdown hooks.
      # Capture each check's status into rc rather than relying on the block's last-command exit: lefthook takes the run
      # script's final exit code, so a chained `lint-no-emdash.sh` (em-dash) failure would otherwise be masked by a
      # passing `dash-lint` (spaced-hyphen) on the next line. Running both unconditionally also surfaces every violation
      # in one pass instead of stopping at the first.
      run: |
        files=$(printf '%s\n' {staged_files})
        if [ -z "$files" ]; then exit 0; fi
        rc=0
        printf '%s\n' "$files" | tr '\n' '\0' | xargs -0 bash tools/lint-no-emdash.sh || rc=1
        printf '%s\n' "$files" | tr '\n' '\0' | go run ./tools/dash-lint || rc=1
        exit $rc

    # Catch secrets before they land. `git secrets` or `trufflehog` could
    # fit here later. For now, a minimal regex guard against the obvious
    # patterns that shouldn't be in source.
    #
    # Reads from the INDEX (`git show :path`) rather than the working tree so
    # a developer who stages a secret and then edits it out of the working
    # tree before committing still gets caught. Falls back to the working
    # tree for files deleted from the index.
    no-secrets:
      glob: "**/*.{go,ts,tsx,js,swift,md,yml,yaml,json,sh}"
      exclude:
        - ".tool-versions"
        - "lefthook.yml"
        - "docs/adr/**"
      run: |
        matched=0
        for f in {staged_files}; do
          content=$(git show ":$f" 2>/dev/null) || continue
          if printf '%s' "$content" | grep -nE 'AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'; then
            echo "Potential secret staged in $f"
            matched=1
          fi
        done
        exit $matched

    # audit_events is append-only. Block any production Go file that
    # stages an UPDATE or DELETE against the table; the audit trail's
    # integrity is the whole point of the schema, and accidental
    # tampering is the regression this hook exists to catch.
    # The Go test at server/identity/internal/audit/append_only_lint_test.go
    # is the CI backstop for developers who bypass with --no-verify.
    # The audit package is NOT excluded (PR #120 review): the regex
    # is UPDATE/DELETE-specific so the legitimate INSERT in mysql.go
    # does not trip, and policing the package is the point.
    audit-append-only:
      glob: "**/*.go"
      exclude:
        - "**/*_test.go"
      run: |
        matched=0
        for f in {staged_files}; do
          content=$(git show ":$f" 2>/dev/null) || continue
          if printf '%s' "$content" | grep -niE '\b(update[[:space:]]+audit_events|delete[[:space:]]+from[[:space:]]+audit_events)\b'; then
            echo "audit_events is append-only; UPDATE/DELETE not permitted in $f"
            matched=1
          fi
        done
        exit $matched

commit-msg:
  commands:
    no-agent-attribution:
      # Commits here are authored by the people who own them. An agent that helped write a change is a tool, and tools do not take
      # authorship credit. This refuses any Co-Authored-By trailer (CONTRIBUTING.md bans them for people and agents alike), an
      # agent session marker, and a "Generated with" line. The only trailer allowed is the one GitHub writes on a squash-merged
      # dependabot PR, which never passes through this hook anyway.
      #
      # This hook covers LOCAL commits only. A squash merge composed by GitHub from a PR body never reaches it, which is how the
      # four commits that prompted this gate got in, so the CI half in .github/workflows/no-agent-attribution.yml is the one that
      # closes that path. Keep both.
      run: tools/lint-no-agent-attribution.sh msg {1}

pre-push:
  parallel: true
  commands:
    go-build:
      # Whole-module build as a cheap smoke test. Failures here almost
      # always mean a refactor left a broken import somewhere. The server
      # embeds server/ui/dist via `embed`; on a clean checkout that dir
      # doesn't exist, so seed a placeholder here (CI workflows do the
      # equivalent before their Go steps).
      run: |
        mkdir -p server/ui/dist && touch server/ui/dist/.gitkeep
        go build ./...

    go-lint-fast:
      # --fast-only enables the quick-running linters (under ~2s usually).
      # The full run still happens in CI and before release. Uses the custom
      # golangci-lint binary at tmp/golangci-lint-custom because .golangci.yml
      # enables the commentwrap plugin which the upstream binary cannot load;
      # falls back to upstream only with --no-config to avoid the unknown-
      # linter error, surfacing a clear stderr hint for the operator.
      run: |
        mkdir -p server/ui/dist && touch server/ui/dist/.gitkeep
        if [ -x tmp/golangci-lint-custom ]; then
          tmp/golangci-lint-custom run --fast-only ./...
        else
          echo "[lefthook] tmp/golangci-lint-custom missing; running upstream golangci-lint --no-config (no commentwrap)" >&2
          echo "[lefthook] Run 'task lint:install' once to enable the full linter set including commentwrap." >&2
          golangci-lint run --no-config --fast-only ./...
        fi

    arch-go:
      # Bounded-context architecture lint via arch-go. Local feedback in
      # ~3s; CI also has its own arch-go workflow as the backstop. Only
      # runs when Go files (or the rule config / Taskfile / arch-go.yml
      # itself) change, mirroring the .github/workflows/arch-go.yml gate.
      glob: "{**/*.go,arch-go.yml,Taskfile.yml,go.mod,go.sum}"
      run: |
        mkdir -p server/ui/dist && touch server/ui/dist/.gitkeep
        task lint:arch

    tsc:
      root: "ui/"
      run: npx tsc --noEmit

# Post-checkout hook keeps UI deps in sync if package-lock.json changed.
post-checkout:
  commands:
    npm-install-if-lock-changed:
      run: |
        if git diff --name-only HEAD@{1} HEAD 2>/dev/null | grep -q '^ui/package-lock.json$'; then
          echo "ui/package-lock.json changed; running npm ci"
          (cd ui && npm ci)
        fi