From 9983f0b8e92785905ec991200c3fc0a4f303e2ee Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 15 Mar 2026 16:51:49 +0100 Subject: [PATCH] feat: expose more options for lefthook --- README.md | 6 ++ packages/repo-lib/lib.nix | 40 ++++---- skills/repo-lib-consumer/references/api.md | 31 +++++++ template/flake.nix | 8 ++ tests/release.sh | 103 ++++++++++++++++++++- 5 files changed, 167 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8419589..f256eac 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ outputs = { self, nixpkgs, repo-lib, ... }: Checks are installed through `lefthook`, with `pre-commit` and `pre-push` commands configured to run in parallel. +For advanced Lefthook features, use raw `config.lefthook` or `perSystem.lefthook`. Those attrsets are merged after generated checks, so you can augment a generated command with fields that the simple `checks` abstraction does not carry, such as `stage_fixed`: + +```nix +config.lefthook.pre-push.commands.tests.stage_fixed = true; +``` + ## Tool banners Tools are declared once. Package-backed tools are added to the shell automatically, and both package-backed and command-backed tools are rendered in the startup banner. diff --git a/packages/repo-lib/lib.nix b/packages/repo-lib/lib.nix index 6db4f96..4244546 100644 --- a/packages/repo-lib/lib.nix +++ b/packages/repo-lib/lib.nix @@ -123,7 +123,7 @@ let required = tool.required or false; }; - normalizeCheck = + checkToLefthookConfig = pkgs: name: rawCheck: let check = { @@ -152,13 +152,13 @@ let then throw "repo-lib: check '${name}' has unsupported stage '${check.stage}'" else - { - enable = true; - entry = "${wrapper}/bin/${wrapperName}"; - pass_filenames = check.passFilenames; - stages = [ check.stage ]; + lib.setAttrByPath [ check.stage "commands" name ] { + run = "${wrapper}/bin/${wrapperName}${hookStageFileArgs check.stage check.passFilenames}"; }; + normalizeLefthookConfig = + label: raw: if builtins.isAttrs raw then raw else throw "repo-lib: ${label} must be an attrset"; + normalizeHookStage = hookName: stage: if @@ -219,16 +219,6 @@ let ) stages ); - hookStages = - hooks: - lib.unique ( - [ - "pre-commit" - "commit-msg" - ] - ++ lib.concatMap (hook: hook.stages or [ "pre-commit" ]) (builtins.attrValues hooks) - ); - parallelHookStageConfig = stage: if @@ -404,6 +394,7 @@ let }, checkSpecs ? { }, rawHookEntries ? { }, + lefthookConfig ? { }, extraPackages ? [ ], }: let @@ -425,13 +416,13 @@ let settings.formatter = { } // formatting.settings; }; - normalizedChecks = lib.mapAttrs (name: check: normalizeCheck pkgs name check) checkSpecs; - hooks = mergeUniqueAttrs "hook" rawHookEntries normalizedChecks; + normalizedLefthookConfig = normalizeLefthookConfig "lefthook config" lefthookConfig; lefthookCheck = lefthookNix.lib.${system}.run { inherit src; config = lib.foldl' lib.recursiveUpdate { } ( [ (parallelHookStageConfig "pre-commit") + (parallelHookStageConfig "pre-push") (lib.setAttrByPath [ "pre-commit" "commands" "treefmt" ] { run = "${treefmtEval.config.build.wrapper}/bin/treefmt --ci {staged_files}"; }) @@ -442,8 +433,9 @@ let run = "${pkgs.gitlint}/bin/gitlint --staged --msg-filename {1}"; }) ] - ++ builtins.map parallelHookStageConfig (hookStages hooks) - ++ lib.mapAttrsToList hookToLefthookConfig hooks + ++ lib.mapAttrsToList (name: check: checkToLefthookConfig pkgs name check) checkSpecs + ++ lib.mapAttrsToList hookToLefthookConfig rawHookEntries + ++ [ normalizedLefthookConfig ] ); }; selectedCheckOutputs = { @@ -641,6 +633,7 @@ rec { settings = { }; }; checks = { }; + lefthook = { }; release = null; } rawConfig; release = @@ -675,6 +668,7 @@ rec { preToolHook ? "", extraShellHook ? "", additionalHooks ? { }, + lefthook ? { }, tools ? [ ], includeStandardPackages ? true, formatters ? { }, @@ -714,6 +708,7 @@ rec { ; formatting = normalizedFormatting; rawHookEntries = additionalHooks; + lefthookConfig = lefthook; shellConfig = shellConfig; tools = legacyTools; extraPackages = @@ -793,6 +788,7 @@ rec { tools = [ ]; shell = { }; checks = { }; + lefthook = { }; packages = { }; apps = { }; } @@ -805,6 +801,9 @@ rec { strictTools = builtins.map (tool: normalizeStrictTool pkgs tool) perSystemResult.tools; duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) strictTools); mergedChecks = mergeUniqueAttrs "check" normalizedConfig.checks perSystemResult.checks; + mergedLefthookConfig = + lib.recursiveUpdate (normalizeLefthookConfig "config.lefthook" normalizedConfig.lefthook) + (normalizeLefthookConfig "perSystem.lefthook" (perSystemResult.lefthook or { })); shellConfig = lib.recursiveUpdate normalizedConfig.shell (perSystemResult.shell or { }); env = if duplicateToolNames != [ ] then @@ -820,6 +819,7 @@ rec { formatting = normalizedConfig.formatting; tools = strictTools; checkSpecs = mergedChecks; + lefthookConfig = mergedLefthookConfig; shellConfig = shellConfig; extraPackages = perSystemResult.shell.packages or [ ]; }; diff --git a/skills/repo-lib-consumer/references/api.md b/skills/repo-lib-consumer/references/api.md index 657ea57..d241f97 100644 --- a/skills/repo-lib-consumer/references/api.md +++ b/skills/repo-lib-consumer/references/api.md @@ -35,6 +35,7 @@ repo-lib.lib.mkRepo { }; checks = { }; + lefthook = { }; release = null; # or attrset below }; @@ -43,6 +44,7 @@ repo-lib.lib.mkRepo { tools = [ ]; shell.packages = [ ]; checks = { }; + lefthook = { }; packages = { }; apps = { }; }; @@ -116,6 +118,34 @@ Rules: - The command is wrapped as a script and connected into `lefthook.nix`. - `pre-commit` and `pre-push` commands are configured to run in parallel. +## Raw Lefthook config + +Use `config.lefthook` or `perSystem.lefthook` for advanced Lefthook features that the built-in `checks` abstraction does not carry. + +Example: + +```nix +{ + checks.tests = { + command = "go test ./..."; + stage = "pre-push"; + }; + + lefthook.pre-push.commands.tests.stage_fixed = true; + + lefthook.commit-msg.commands.commitlint = { + run = "pnpm commitlint --edit {1}"; + stage_fixed = true; + }; +} +``` + +Rules: + +- These attrsets are passed through to `lefthook.nix`. +- They are merged after generated checks, so they can extend generated commands. +- Prefer `checks` for the simple common case and `lefthook` for advanced fields such as `stage_fixed`, `files`, `glob`, `exclude`, `jobs`, or `scripts`. + ## Tools Preferred shape in `perSystem.tools`: @@ -270,6 +300,7 @@ If the user asks for a webhook after the tag exists remotely: - `extraPackages` - `preToolHook` - `extraShellHook` +- `lefthook` - `additionalHooks` - old `tools = [ { name; bin; versionCmd; color; } ]` - `features.oxfmt` diff --git a/template/flake.nix b/template/flake.nix index 1f06efe..e3888bd 100644 --- a/template/flake.nix +++ b/template/flake.nix @@ -67,6 +67,14 @@ # }; }; + # For advanced Lefthook fields like `stage_fixed`, use raw passthrough. + # repo-lib merges this after generated checks. + # lefthook.pre-push.commands.tests.stage_fixed = true; + # lefthook.commit-msg.commands.commitlint = { + # run = "pnpm commitlint --edit {1}"; + # stage_fixed = true; + # }; + # repo-lib also installs built-in hooks for: # - treefmt / nixfmt on `pre-commit` # - gitleaks on `pre-commit` diff --git a/tests/release.sh b/tests/release.sh index 0655c7c..b5fad22 100755 --- a/tests/release.sh +++ b/tests/release.sh @@ -263,6 +263,39 @@ write_mk_repo_command_tool_flake() { EOF } +write_mk_repo_lefthook_flake() { + local repo_dir="$1" + cat >"$repo_dir/flake.nix" <"$repo_dir/flake.nix" <&2 } +run_stable_then_beta_cannot_reuse_same_base_case() { + local case_name="stable release cannot go back to same-base beta" + + local workdir + workdir="$(mktemp -d)" + local repo_dir="$workdir/repo" + local remote_dir="$workdir/remote.git" + CURRENT_LOG="$workdir/case.log" + + prepare_case_repo "$repo_dir" "$remote_dir" + + run_capture_ok "$case_name: initial beta release failed" run_release "$repo_dir" beta + run_capture_ok "$case_name: stable promotion failed" run_release "$repo_dir" full + run_capture_ok "$case_name: second beta release failed" run_release "$repo_dir" beta + + local got_version + got_version="$(version_from_file "$repo_dir")" + assert_eq "1.0.2-beta.1" "$got_version" "$case_name: VERSION mismatch" + + if ! git -C "$repo_dir" tag --list | grep -qx "v1.0.1"; then + fail "$case_name: expected stable tag v1.0.1 was not created" + fi + + if ! git -C "$repo_dir" tag --list | grep -qx "v1.0.2-beta.1"; then + fail "$case_name: expected tag v1.0.2-beta.1 was not created" + fi + + rm -rf "$workdir" + CURRENT_LOG="" + echo "[test] PASS: $case_name" >&2 +} + run_set_stable_then_full_noop_case() { local case_name="set stable then full fails with no-op" @@ -1158,6 +1223,38 @@ run_mk_repo_command_tool_case() { echo "[test] PASS: $case_name" >&2 } +run_mk_repo_lefthook_case() { + local case_name="mkRepo exposes raw lefthook config for advanced hook fields" + local workdir + workdir="$(mktemp -d)" + local repo_dir="$workdir/mk-repo-lefthook" + local system + local derivation_json="$workdir/lefthook-run.drv.json" + local lefthook_yml_drv + local lefthook_yml_json="$workdir/lefthook-yml.drv.json" + mkdir -p "$repo_dir" + write_mk_repo_lefthook_flake "$repo_dir" + CURRENT_LOG="$workdir/mk-repo-lefthook.log" + + system="$(nix eval --raw --impure --expr 'builtins.currentSystem')" + run_capture_ok "$case_name: flake show failed" nix flake show --json --no-write-lock-file "$repo_dir" + run_capture_ok "$case_name: lefthook derivation show failed" bash -c 'nix derivation show "$1" >"$2"' _ "$repo_dir#checks.${system}.lefthook-check" "$derivation_json" + + lefthook_yml_drv="$(perl -0ne 'print "/nix/store/$1\n" if /"([a-z0-9]{32}-lefthook\.yml\.drv)"/' "$derivation_json")" + if [[ -z "$lefthook_yml_drv" ]]; then + fail "$case_name: could not locate lefthook.yml derivation" + fi + + run_capture_ok "$case_name: lefthook.yml derivation show failed" bash -c 'nix derivation show "$1" >"$2"' _ "$lefthook_yml_drv" "$lefthook_yml_json" + assert_contains '\"pre-push\":{\"commands\":{\"tests\":{' "$lefthook_yml_json" "$case_name: generated check missing from pre-push" + assert_contains 'repo-lib-check-tests' "$lefthook_yml_json" "$case_name: generated check command missing from lefthook config" + assert_contains '\"stage_fixed\":true' "$lefthook_yml_json" "$case_name: stage_fixed missing from lefthook config" + + rm -rf "$workdir" + CURRENT_LOG="" + echo "[test] PASS: $case_name" >&2 +} + run_mk_repo_tool_failure_case() { local case_name="mkRepo required tools fail shell startup" local workdir @@ -1264,6 +1361,7 @@ EOF run_case "channel-only from stable bumps patch" "beta" "1.0.1-beta.1" run_case "explicit minor bump keeps requested bump" "minor beta" "1.1.0-beta.1" run_set_prerelease_then_full_case +run_stable_then_beta_cannot_reuse_same_base_case run_set_stable_then_full_noop_case run_set_stable_from_prerelease_requires_full_case run_patch_stable_from_prerelease_requires_full_case @@ -1271,11 +1369,14 @@ run_structured_release_steps_case run_version_metadata_case run_mk_repo_case run_mk_repo_command_tool_case +run_mk_repo_lefthook_case run_mk_repo_tool_failure_case run_impure_bootstrap_validation_case run_legacy_api_eval_case run_template_eval_case run_release_replace_backref_case -run_randomized_quickcheck_cases +if [[ "${QUICKCHECK:-0}" == "1" ]]; then + run_randomized_quickcheck_cases +fi echo "[test] All release tests passed" >&2