diff --git a/README.md b/README.md index a566571..073ecc5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ inherit configs and policies via the mechanisms below. | Renovate `extends` | `renovate.json` in each repo: `extends: github>JacobPEvans/.github:renovate-presets` (this repo's `renovate.json` is the example) | | Biome config | Each repo carries a copy of `biome.jsonc` scaffolded from this repo; Renovate keeps it in sync | | markdownlint config | Each repo carries a copy of `.markdownlint-cli2.yaml` from this repo; sync TBD (manual for now) | +| Pre-commit hooks (shared) | `precommit/` — Nix flake import or static YAML copy; see [`precommit/README.md`](precommit/README.md) | | AI assistant policy | `CLAUDE.md` — read by Claude Code on every session | ## Usage @@ -101,6 +102,8 @@ This repo exposes the following inheritance surfaces: | `biome.jsonc` | Canonical Biome lint + format config (code) | | `.markdownlint-cli2.yaml` | Canonical markdownlint-cli2 config (`.md` files) | | `renovate.json` | Org-default Renovate extending JacobPEvans presets | +| `precommit/` | Shared pre-commit layer (canonical lint configs + static YAML templates); see [`precommit/README.md`](precommit/README.md) | +| `zizmor.yml` | Org-wide zizmor workflow-security policy (referenced by the pre-commit `zizmor` hook) | | `SECURITY.md` | Org-wide vulnerability reporting policy (auto-applied to every dryvist repo's Security tab) | | `profile/README.md` | Org profile page at | diff --git a/precommit/README.md b/precommit/README.md new file mode 100644 index 0000000..2051e83 --- /dev/null +++ b/precommit/README.md @@ -0,0 +1,176 @@ +# precommit — org-wide pre-commit shared layer + +Single source of truth for pre-commit hook configuration across every +dryvist repo. Two consumption paths share these artifacts. + +- **Nix flake module** — Repos with `flake.nix` + `.envrc` (terraform-*, + ansible-*, nix-*, orbstack-kubernetes) consume + `inputs.nix-devenv.flakeModules.{base,terraform,ansible,python,nix,markdown}`. + Versions pinned via `flake.lock`. +- **Static YAML copy** — Repos without Nix (cribl packs, raw CI + checkouts, external contributor clones) copy `templates/.yaml` + at scaffold time. Renovate keeps `rev:` pins fresh. + +Both paths reference the same canonical config files in `configs/` for +tools that demand a config file on disk (ansible-lint, yamllint, tflint). +Markdown lint config lives at the dryvist/.github root +(`../.markdownlint-cli2.yaml`) for backwards compatibility with existing +markdownlint workflow consumers. + +## Layout + +```text +precommit/ +├── configs/ +│ ├── ansible-lint.yml # canonical .ansible-lint +│ ├── tflint.hcl # canonical .tflint.hcl +│ └── yamllint.yml # canonical .yamllint.yml +├── templates/ +│ ├── base.yaml # common 80% (no language-specific hooks) +│ ├── terraform.yaml # base + terraform_fmt/validate/tflint/docs +│ ├── ansible.yaml # base + ansible-lint + yamllint +│ └── python.yaml # base + ruff + ruff-format + mypy +└── README.md +``` + +The markdownlint canonical (`.markdownlint-cli2.yaml`) stays at the +repo root — it predates this directory and is referenced by existing +consumer workflows. New canonical configs land here in `configs/`. + +## Installation + +This directory is consumed by reference, not installed. Pick the path +that matches the consumer repo once. + +### Add to a Nix-flake repo + +No copy step needed. Add the `nix-devenv` flake input and one +`imports = [ ... ]` line in the consumer's `flake.nix` (see Usage). + +### Add to a non-Nix repo + +Fetch the matching template at scaffold time, plus the configs the +hooks need: + +```bash +# Pick base / terraform / ansible / python +gh api repos/dryvist/.github/contents/precommit/templates/terraform.yaml \ + -H "Accept: application/vnd.github.raw" > .pre-commit-config.yaml + +# Materialize tflint canonical (terraform profile only) +gh api repos/dryvist/.github/contents/precommit/configs/tflint.hcl \ + -H "Accept: application/vnd.github.raw" > .tflint.hcl + +# Markdown lint config lives at the .github root +gh api repos/dryvist/.github/contents/.markdownlint-cli2.yaml \ + -H "Accept: application/vnd.github.raw" > .markdownlint-cli2.yaml + +# Zizmor policy for workflow security +gh api repos/dryvist/.github/contents/zizmor.yml \ + -H "Accept: application/vnd.github.raw" > zizmor.yml + +pre-commit install +``` + +For `ansible.yaml`, also fetch `precommit/configs/ansible-lint.yml` +to `.ansible-lint` and `precommit/configs/yamllint.yml` to +`.yamllint.yml`. For `python.yaml`, no extra configs needed beyond +optional repo-local `pyproject.toml` for mypy / ruff. + +## Usage + +### Run hooks in a Nix-flake repo + +In the consumer's `flake.nix`: + +```nix +{ + inputs.nix-devenv.url = "github:dryvist/nix-devenv"; + inputs.flake-parts.url = "github:hercules-ci/flake-parts"; + + outputs = inputs@{ flake-parts, nix-devenv, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "aarch64-darwin" "x86_64-linux" ]; + imports = [ inputs.nix-devenv.flakeModules.terraform ]; + }; +} +``` + +Profile picks one of `{base, terraform, ansible, python, nix, markdown}`. +The module imports `dev-hygiene` (base hooks) and layers the matching +language hooks on top. No `.pre-commit-config.yaml` needed in the repo. + +`base`, `nix`, and `markdown` are aliases for `dev-hygiene` — the base +already covers Nix lints (deadnix, statix) and markdownlint via +file-glob filtering. Repos pick one of those aliases for readability; +the runtime behavior is identical. + +### Run hooks in a non-Nix repo + +After running the Installation step above, hooks run automatically on +`git commit`. To run them ad-hoc: + +```bash +pre-commit run --all-files +``` + +Renovate's custom manager in dryvist/.github keeps `rev:` pins in the +copied `.pre-commit-config.yaml` fresh going forward. Sync of the +config files themselves (`.tflint.hcl`, etc.) is still manual today — +extending the Renovate custom manager to cover them is tracked +separately. + +## API + +This directory exposes the following surfaces: + +| Path | Purpose | +| --- | --- | +| `configs/ansible-lint.yml` | Canonical ansible-lint (production profile, fqcn + no-changed-when) | +| `configs/tflint.hcl` | Canonical tflint (terraform plugin, recommended preset, docs rules) | +| `configs/yamllint.yml` | Canonical yamllint (line-length 160 warn, octal forbidden, ansible-lint-compatible) | +| `templates/base.yaml` | Common-80% static `.pre-commit-config.yaml` for non-Nix consumers | +| `templates/terraform.yaml` | `base` plus terraform_fmt / validate / tflint / docs | +| `templates/ansible.yaml` | `base` plus ansible-lint + yamllint | +| `templates/python.yaml` | `base` plus ruff / ruff-format / mypy | + +## Contributing + +Adding a new profile means both paths stay in sync: + +1. **Nix side** — add `flake-modules/profiles/.nix` in + [`dryvist/nix-devenv`](https://github.com/dryvist/nix-devenv) and + expose `flakeModules.` in its `flake.nix`. Validate with a + smoke-test consumer under `tests/profile-modules//`. +2. **YAML side** — add `templates/.yaml` here that mirrors the + Nix profile's hook set. +3. Document both paths in this README's `Layout` section. + +Profile hooks must stay in sync between the two paths. The Nix smoke +tests in `dryvist/nix-devenv` catch hook-name drift in git-hooks.nix; +no equivalent test exists for the YAML templates yet (a `pre-commit +dry-run` against a representative repo is the manual check). + +### Canonical-config choices + +- `configs/` holds files that tools demand on disk. Tools that accept + all config via CLI args ship their canonical at the repo root (see + `.markdownlint-cli2.yaml`). +- Each merged config picks the **most-common variant** from the + inventory rather than the strictest superset, to minimize migration + churn. Consumers add stricter rules locally until a critical mass + agrees to lift them into the canonical. +- AWS / GCP / Azure tflint plugins stay opt-in per repo. Their + rulesets are large and noisy on repos that don't target that cloud. + The canonical `tflint.hcl` enables only the core terraform plugin. +- `bandit` and `detect-secrets` appear in one inventory repo each and + are NOT in the python template. Repos that want them add a `repo:` + block locally. +- `checkov` for terraform appears in three repos and is NOT in the + terraform template. Run time dominates the hook cycle; consumers + opt in locally via `pre-commit.settings.hooks.checkov.enable = true;` + (Nix path) or a `repo:` block (YAML path). + +## License + +[Apache-2.0](../LICENSE) — inherited from the dryvist/.github root. diff --git a/precommit/configs/ansible-lint.yml b/precommit/configs/ansible-lint.yml new file mode 100644 index 0000000..3ee36a9 --- /dev/null +++ b/precommit/configs/ansible-lint.yml @@ -0,0 +1,44 @@ +--- +# dryvist organization-wide ansible-lint canonical. +# +# Single source of truth for ansible-lint across every dryvist ansible +# repo. Consumer repos either fetch this file at scaffold time (non-Nix +# path) or have the Nix-side `fetch-shared-configs` helper materialize +# it at devShell entry (Nix path). +# +# Schema: https://ansible.readthedocs.io/projects/lint/configuring/ + +profile: production + +exclude_paths: + - .git/ + - .github/ + - .cache/ + - .venv/ + - venv/ + # SOPS-encrypted files are opaque ciphertext + metadata; linting them + # is meaningless. The recursive glob also catches future encrypted + # files in any subdirectory. + - "**/*.enc.yaml" + - "**/*.enc.yml" + +# Noise from yaml line-length is handled by .yamllint's line-length rule +# at warning level; ansible-lint should not duplicate that complaint. +skip_list: + - yaml[line-length] + +warn_list: + - experimental + +enable_list: + - fqcn + - no-changed-when + +# File-type overrides for files in non-standard locations. Repos that +# don't have these files are unaffected — ansible-lint ignores absent +# entries. +kinds: + - playbook: "**/load_terraform.yml" + +use_default_rules: true +offline: false diff --git a/precommit/configs/tflint.hcl b/precommit/configs/tflint.hcl new file mode 100644 index 0000000..47d1b55 --- /dev/null +++ b/precommit/configs/tflint.hcl @@ -0,0 +1,43 @@ +# dryvist organization-wide tflint canonical. +# +# Single source of truth for tflint configuration across every dryvist +# terraform/opentofu repo. Consumer repos either fetch this file at +# scaffold time (non-Nix path) or have the Nix-side `fetch-shared-configs` +# helper materialize it into the repo at devShell entry (Nix path). +# +# The terraform plugin is enabled with the `recommended` preset. The +# rules below are an explicit superset of the most-common per-repo +# variants (terraform-proxmox / terraform-github style): documented +# variables/outputs, required providers/version. AWS / GCP / Azure +# plugins stay opt-in per repo because they pull large rulesets and +# slow tflint considerably on repos that don't target that cloud. +# +# Schema: https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/config.md + +config { + format = "compact" + call_module_type = "local" + force = false +} + +plugin "terraform" { + enabled = true + preset = "recommended" +} + +# Documentation-discipline rules (most-shared canonical). +rule "terraform_documented_variables" { + enabled = true +} + +rule "terraform_documented_outputs" { + enabled = true +} + +rule "terraform_required_providers" { + enabled = true +} + +rule "terraform_required_version" { + enabled = true +} diff --git a/precommit/configs/yamllint.yml b/precommit/configs/yamllint.yml new file mode 100644 index 0000000..6e070a0 --- /dev/null +++ b/precommit/configs/yamllint.yml @@ -0,0 +1,54 @@ +--- +# dryvist organization-wide yamllint canonical. +# +# Single source of truth for yamllint across every dryvist repo with +# YAML content (which is most of them). Consumer repos either fetch +# this at scaffold time (non-Nix path) or have the Nix-side +# `fetch-shared-configs` helper materialize it at devShell entry +# (Nix path). +# +# Rule choices represent the most-common variant across the inventory +# (ansible-proxmox style). Repos that want a different line-length or +# stricter truthy rules can override locally. +# +# Schema: https://yamllint.readthedocs.io/en/stable/configuration.html + +extends: default + +ignore: | + .github/aw/ + .github/workflows/*.lock.yml + .github/workflows/agentics-maintenance.yml + # SOPS-encrypted files: opaque ciphertext; the recursive glob also + # catches future encrypted files in any subdirectory. + **/*.enc.yaml + **/*.enc.yml + +rules: + line-length: + max: 160 + level: warning + truthy: + allowed-values: + - "true" + - "false" + - "yes" + - "no" + check-keys: false + comments: + min-spaces-from-content: 1 + # Required by ansible-lint compatibility — ansible playbooks frequently + # put comments at the same indent as the surrounding content. + comments-indentation: false + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + indentation: + spaces: 2 + indent-sequences: true + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true diff --git a/precommit/templates/ansible.yaml b/precommit/templates/ansible.yaml new file mode 100644 index 0000000..9868ab9 --- /dev/null +++ b/precommit/templates/ansible.yaml @@ -0,0 +1,56 @@ +# dryvist ansible pre-commit template (non-Nix consumers). +# +# Includes the base hook set plus ansible-lint + yamllint. Both consume +# canonical configs from dryvist/.github at precommit/configs/, which +# the consumer materializes alongside .pre-commit-config.yaml: +# +# gh api repos/dryvist/.github/contents/precommit/configs/ansible-lint.yml \ +# -H "Accept: application/vnd.github.raw" > .ansible-lint +# gh api repos/dryvist/.github/contents/precommit/configs/yamllint.yml \ +# -H "Accept: application/vnd.github.raw" > .yamllint.yml +# +# Nix-flake consumers should NOT use this file. They get equivalent +# coverage via `imports = [ inputs.nix-devenv.flakeModules.ansible ];`. +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + args: [--maxkb=500] + - id: end-of-file-fixer + - id: trim-trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: detect-private-key + + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.22.1 + hooks: + - id: markdownlint-cli2 + exclude: ^CHANGELOG\.md$ + + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor + files: ^\.github/workflows/.*\.ya?ml$ + args: + - --persona=regular + - --min-severity=medium + - --min-confidence=medium + - --config + - zizmor.yml + + - repo: https://github.com/ansible/ansible-lint + rev: v26.4.0 + hooks: + - id: ansible-lint + + - repo: https://github.com/adrienverge/yamllint + rev: v1.38.0 + hooks: + - id: yamllint diff --git a/precommit/templates/base.yaml b/precommit/templates/base.yaml new file mode 100644 index 0000000..6e3cb2f --- /dev/null +++ b/precommit/templates/base.yaml @@ -0,0 +1,52 @@ +# dryvist org-wide pre-commit base template (non-Nix consumers). +# +# Nix-flake consumers should NOT use this file. They get the same hooks +# via `imports = [ inputs.nix-devenv.flakeModules.base ];` with version +# pinning handled by flake.lock. +# +# Non-Nix consumers fetch this file at scaffold time and let Renovate +# keep `rev:` pins fresh: +# +# gh api repos/dryvist/.github/contents/precommit/templates/base.yaml \ +# -H "Accept: application/vnd.github.raw" > .pre-commit-config.yaml +# +# Hook set mirrors lib/pre-commit-hooks.nix in dryvist/nix-devenv so that +# Nix and non-Nix consumers run an identical baseline. +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + args: [--maxkb=500] + - id: end-of-file-fixer + - id: trim-trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: detect-private-key + + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.22.1 + hooks: + - id: markdownlint-cli2 + # Picks up .markdownlint-cli2.yaml from the repo root (the + # canonical version copied from dryvist/.github). + exclude: ^CHANGELOG\.md$ + + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor + files: ^\.github/workflows/.*\.ya?ml$ + args: + - --persona=regular + - --min-severity=medium + - --min-confidence=medium + - --config + # The org-wide zizmor policy lives next to this file in the + # dryvist/.github root. Repos must materialize it at the + # same path or override with --config . + - zizmor.yml diff --git a/precommit/templates/python.yaml b/precommit/templates/python.yaml new file mode 100644 index 0000000..3f106a2 --- /dev/null +++ b/precommit/templates/python.yaml @@ -0,0 +1,55 @@ +# dryvist python pre-commit template (non-Nix consumers). +# +# Includes the base hook set plus ruff (lint + format) and mypy. +# bandit / detect-secrets are intentionally NOT in this template — +# they were used in 1 inventory repo each and slow the hook cycle +# without proportional value. Repos that want them add the relevant +# `repo:` block locally. +# +# Nix-flake consumers should NOT use this file. They get equivalent +# coverage via `imports = [ inputs.nix-devenv.flakeModules.python ];`. +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + args: [--maxkb=500] + - id: end-of-file-fixer + - id: trim-trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: detect-private-key + + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.22.1 + hooks: + - id: markdownlint-cli2 + exclude: ^CHANGELOG\.md$ + + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor + files: ^\.github/workflows/.*\.ya?ml$ + args: + - --persona=regular + - --min-severity=medium + - --min-confidence=medium + - --config + - zizmor.yml + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.6 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.16.1 + hooks: + - id: mypy diff --git a/precommit/templates/terraform.yaml b/precommit/templates/terraform.yaml new file mode 100644 index 0000000..7c2caca --- /dev/null +++ b/precommit/templates/terraform.yaml @@ -0,0 +1,64 @@ +# dryvist terraform pre-commit template (non-Nix consumers). +# +# Includes the base hook set plus terraform-specific static checks. Hook +# choices follow the terraform-checks-placement policy: +# +# * `terraform_fmt` and `terraform_validate` (with init -backend=false) +# are credential-free and safe in pre-commit. +# * `tflint` reads the canonical config from dryvist/.github at +# precommit/configs/tflint.hcl — materialize that file alongside +# .pre-commit-config.yaml or override args. +# * `terraform_docs` runs in pre-commit AND is mirrored in CI. +# * `terraform plan`/`apply` are NOT in pre-commit (credentialed; CI-only via OIDC). +# +# Nix-flake consumers should NOT use this file. They get equivalent +# coverage via `imports = [ inputs.nix-devenv.flakeModules.terraform ];`. +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + args: [--maxkb=500] + - id: end-of-file-fixer + - id: trim-trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: detect-private-key + + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.22.1 + hooks: + - id: markdownlint-cli2 + exclude: ^CHANGELOG\.md$ + + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor + files: ^\.github/workflows/.*\.ya?ml$ + args: + - --persona=regular + - --min-severity=medium + - --min-confidence=medium + - --config + - zizmor.yml + + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.92.0 + hooks: + - id: terraform_fmt + - id: terraform_validate + args: + - --hook-config=--retry-once-with-cleanup=true + - --tf-init-args=-backend=false + - id: terraform_tflint + args: + # Materialize precommit/configs/tflint.hcl into the repo as + # `.tflint.hcl` (root-level) at scaffold time, then this hook + # finds it without further flags. + - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl + - id: terraform_docs