5 Commits

Author SHA1 Message Date
eric
71c7fe09cd chore(release): v3.4.0 2026-03-15 17:15:23 +01:00
eric
45f3830794 ci: limit lefthook logging 2026-03-15 17:14:25 +01:00
eric
b8d0a69d4d fix: lefthook logging 2026-03-15 17:10:26 +01:00
eric
c5f8ee6005 chore(release): v3.3.0 2026-03-15 16:55:02 +01:00
eric
9983f0b8e9 feat: expose more options for lefthook 2026-03-15 16:51:49 +01:00
8 changed files with 247 additions and 29 deletions

1
.envrc
View File

@@ -1 +1,2 @@
watch_file flake.nix
use flake

View File

@@ -15,7 +15,7 @@
## Use the template
```bash
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.2.0#default' --refresh
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.4.0#default' --refresh
```
## Use the library
@@ -23,7 +23,7 @@ nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/ta
Add this flake input:
```nix
inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.2.0";
inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.4.0";
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
```
@@ -73,6 +73,13 @@ outputs = { self, nixpkgs, repo-lib, ... }:
- merged `packages` and `apps` from `perSystem`
Checks are installed through `lefthook`, with `pre-commit` and `pre-push` commands configured to run in parallel.
repo-lib also sets Lefthook `output = [ "failure" "summary" ]` by default.
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

View File

@@ -1,4 +1,4 @@
3.2.0
3.4.0
stable
0

View File

@@ -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
@@ -424,16 +415,27 @@ let
// formatting.programs;
settings.formatter = { } // formatting.settings;
};
treefmtWrapper = treefmtEval.config.build.wrapper;
lefthookBinWrapper = pkgs.writeShellScript "lefthook-dumb-term" ''
exec env TERM=dumb ${lib.getExe pkgs.lefthook} "$@"
'';
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 { } (
[
{
output = [
"failure"
"summary"
];
}
(parallelHookStageConfig "pre-commit")
(parallelHookStageConfig "pre-push")
(lib.setAttrByPath [ "pre-commit" "commands" "treefmt" ] {
run = "${treefmtEval.config.build.wrapper}/bin/treefmt --ci {staged_files}";
run = "${treefmtWrapper}/bin/treefmt --no-cache {staged_files}";
stage_fixed = true;
})
(lib.setAttrByPath [ "pre-commit" "commands" "gitleaks" ] {
run = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
@@ -442,11 +444,13 @@ 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 = {
formatting-check = treefmtEval.config.build.check src;
hook-check = lefthookCheck;
lefthook-check = lefthookCheck;
};
@@ -557,10 +561,17 @@ let
in
{
checks = selectedCheckOutputs;
formatter = treefmtEval.config.build.wrapper;
formatter = treefmtWrapper;
shell = pkgs.mkShell {
LEFTHOOK_BIN = builtins.toString lefthookBinWrapper;
packages = lib.unique (
selectedStandardPackages ++ extraPackages ++ toolPackages ++ [ pkgs.lefthook ]
selectedStandardPackages
++ extraPackages
++ toolPackages
++ [
pkgs.lefthook
treefmtWrapper
]
);
shellHook = buildShellHook {
hooksShellHook = lefthookCheck.shellHook;
@@ -641,6 +652,7 @@ rec {
settings = { };
};
checks = { };
lefthook = { };
release = null;
} rawConfig;
release =
@@ -675,6 +687,7 @@ rec {
preToolHook ? "",
extraShellHook ? "",
additionalHooks ? { },
lefthook ? { },
tools ? [ ],
includeStandardPackages ? true,
formatters ? { },
@@ -714,6 +727,7 @@ rec {
;
formatting = normalizedFormatting;
rawHookEntries = additionalHooks;
lefthookConfig = lefthook;
shellConfig = shellConfig;
tools = legacyTools;
extraPackages =
@@ -793,6 +807,7 @@ rec {
tools = [ ];
shell = { };
checks = { };
lefthook = { };
packages = { };
apps = { };
}
@@ -805,6 +820,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 +838,7 @@ rec {
formatting = normalizedConfig.formatting;
tools = strictTools;
checkSpecs = mergedChecks;
lefthookConfig = mergedLefthookConfig;
shellConfig = shellConfig;
extraPackages = perSystemResult.shell.packages or [ ];
};

View File

@@ -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`

View File

@@ -1 +1,2 @@
watch_file flake.nix
use flake

View File

@@ -4,7 +4,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.2.0";
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.4.0";
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
};
@@ -53,6 +53,7 @@
# These checks become lefthook commands in the generated `lefthook.yml`.
# repo-lib runs `pre-commit` and `pre-push` hook commands in parallel.
# It also sets `output = [ "failure" "summary" ]` by default.
checks = {
tests = {
command = "echo 'No tests defined yet.'";
@@ -67,6 +68,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`

View File

@@ -35,7 +35,7 @@ assert_contains() {
local needle="$1"
local haystack_file="$2"
local message="$3"
if ! grep -Fq "$needle" "$haystack_file"; then
if ! grep -Fq -- "$needle" "$haystack_file"; then
fail "$message (missing '$needle')"
fi
}
@@ -263,6 +263,49 @@ write_mk_repo_command_tool_flake() {
EOF
}
write_mk_repo_lefthook_flake() {
local repo_dir="$1"
cat >"$repo_dir/flake.nix" <<EOF
{
description = "mkRepo raw lefthook config";
inputs = {
nixpkgs.url = "path:${NIXPKGS_FLAKE_PATH}";
repo-lib.url = "path:${ROOT_DIR}";
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, repo-lib, ... }:
repo-lib.lib.mkRepo {
inherit self nixpkgs;
src = ./.;
config = {
checks.tests = {
command = "echo test";
stage = "pre-push";
passFilenames = false;
};
lefthook.pre-push.commands.tests.stage_fixed = true;
release.steps = [ ];
};
};
}
EOF
}
init_git_repo() {
local repo_dir="$1"
run_capture_ok "init_git_repo: git init failed" git -C "$repo_dir" init
run_capture_ok "init_git_repo: git config user.name failed" git -C "$repo_dir" config user.name "Repo Lib Test"
run_capture_ok "init_git_repo: git config user.email failed" git -C "$repo_dir" config user.email "repo-lib-test@example.com"
run_capture_ok "init_git_repo: git add failed" git -C "$repo_dir" add flake.nix
run_capture_ok "init_git_repo: git commit failed" git -C "$repo_dir" commit -m "init"
}
write_tool_failure_flake() {
local repo_dir="$1"
cat >"$repo_dir/flake.nix" <<EOF
@@ -868,6 +911,38 @@ run_set_prerelease_then_full_case() {
echo "[test] PASS: $case_name" >&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 +1233,76 @@ 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 '\"output\":[\"failure\",\"summary\"]' "$lefthook_yml_json" "$case_name: lefthook output config missing"
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_treefmt_hook_case() {
local case_name="mkRepo configures treefmt and lefthook for dev shell hooks"
local workdir
workdir="$(mktemp -d)"
local repo_dir="$workdir/mk-repo-treefmt"
local system
local derivation_json="$workdir/treefmt-hook.drv.json"
local lefthook_yml_drv
local lefthook_yml_json="$workdir/treefmt-hook-yml.drv.json"
mkdir -p "$repo_dir"
write_mk_repo_flake "$repo_dir"
CURRENT_LOG="$workdir/mk-repo-treefmt.log"
init_git_repo "$repo_dir"
run_capture_ok "$case_name: treefmt should be available in shell" bash -c 'cd "$1" && nix develop --no-write-lock-file . -c sh -c '"'"'printf "%s\n" "$LEFTHOOK_BIN" && command -v treefmt'"'"'' _ "$repo_dir"
assert_contains 'lefthook-dumb-term' "$CURRENT_LOG" "$case_name: LEFTHOOK_BIN wrapper missing"
assert_contains '/bin/treefmt' "$CURRENT_LOG" "$case_name: treefmt missing from shell"
system="$(nix eval --raw --impure --expr 'builtins.currentSystem')"
run_capture_ok "$case_name: formatting check derivation show failed" bash -c 'nix derivation show "$1" >"$2"' _ "$repo_dir#checks.${system}.formatting-check" "$workdir/formatting-check.drv.json"
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 '--no-cache {staged_files}' "$lefthook_yml_json" "$case_name: treefmt hook missing staged-file format command"
assert_contains '\"stage_fixed\":true' "$lefthook_yml_json" "$case_name: treefmt hook should re-stage formatted files"
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 +1409,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 +1417,15 @@ 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_treefmt_hook_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
if [[ "${QUICKCHECK:-0}" == "1" ]]; then
run_randomized_quickcheck_cases
fi
echo "[test] All release tests passed" >&2