4 Commits

Author SHA1 Message Date
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
eric
0d339e2de0 chore(release): v3.2.0 2026-03-15 16:31:43 +01:00
eric
7dcb0d1b3a feat: replace githooks with lefthook 2026-03-15 16:31:32 +01:00
12 changed files with 355 additions and 128 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.pre-commit-config.yaml .pre-commit-config.yaml
lefthook.yml
.direnv .direnv
result result
template/flake.lock template/flake.lock

View File

@@ -15,7 +15,7 @@
## Use the template ## Use the template
```bash ```bash
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.1.0#default' --refresh nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.3.0#default' --refresh
``` ```
## Use the library ## 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: Add this flake input:
```nix ```nix
inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.1.0"; inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.3.0";
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs"; inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
``` ```
@@ -66,11 +66,20 @@ outputs = { self, nixpkgs, repo-lib, ... }:
`mkRepo` generates: `mkRepo` generates:
- `devShells.${system}.default` - `devShells.${system}.default`
- `checks.${system}.pre-commit-check` - `checks.${system}.hook-check`
- `checks.${system}.lefthook-check`
- `formatter.${system}` - `formatter.${system}`
- `packages.${system}.release` when `config.release != null` - `packages.${system}.release` when `config.release != null`
- merged `packages` and `apps` from `perSystem` - merged `packages` and `apps` from `perSystem`
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 ## 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. 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.

View File

@@ -1,4 +1,4 @@
3.1.0 3.3.0
stable stable
0 0

77
flake.lock generated
View File

@@ -1,79 +1,26 @@
{ {
"nodes": { "nodes": {
"flake-compat": { "lefthook-nix": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1772024342,
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"git-hooks",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1709087332, "lastModified": 1770377107,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", "narHash": "sha256-/QEXSDeAo5RK81PtM0yDhmt9k3v1/pse/jsrT1yXNhU=",
"owner": "hercules-ci", "owner": "sudosubin",
"repo": "gitignore.nix", "repo": "lefthook.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "rev": "9cdaf7ce95ae77cbabc5b556bdd35d3cf0b849f5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "hercules-ci", "owner": "sudosubin",
"repo": "gitignore.nix", "repo": "lefthook.nix",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": {
"lastModified": 1770073757,
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1772542754, "lastModified": 1772542754,
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
@@ -89,7 +36,7 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_3": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1770107345, "lastModified": 1770107345,
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
@@ -107,14 +54,14 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"git-hooks": "git-hooks", "lefthook-nix": "lefthook-nix",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
} }
}, },
"treefmt-nix": { "treefmt-nix": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_3" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1770228511, "lastModified": 1770228511,

View File

@@ -4,7 +4,8 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
git-hooks.url = "github:cachix/git-hooks.nix"; lefthook-nix.url = "github:sudosubin/lefthook.nix";
lefthook-nix.inputs.nixpkgs.follows = "nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix"; treefmt-nix.url = "github:numtide/treefmt-nix";
}; };
@@ -13,13 +14,14 @@
self, self,
nixpkgs, nixpkgs,
treefmt-nix, treefmt-nix,
git-hooks, lefthook-nix,
... ...
}: }:
let let
lib = nixpkgs.lib; lib = nixpkgs.lib;
repoLib = import ./packages/repo-lib/lib.nix { repoLib = import ./packages/repo-lib/lib.nix {
inherit nixpkgs treefmt-nix git-hooks; inherit nixpkgs treefmt-nix;
lefthookNix = lefthook-nix;
releaseScriptPath = ./packages/release/release.sh; releaseScriptPath = ./packages/release/release.sh;
shellHookTemplatePath = ./packages/repo-lib/shell-hook.sh; shellHookTemplatePath = ./packages/repo-lib/shell-hook.sh;
}; };
@@ -94,6 +96,7 @@
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
bash bash
git git
nix
gnused gnused
coreutils coreutils
gnugrep gnugrep

View File

@@ -1,7 +1,7 @@
{ {
nixpkgs, nixpkgs,
treefmt-nix, treefmt-nix,
git-hooks, lefthookNix,
releaseScriptPath ? ./release.sh, releaseScriptPath ? ./release.sh,
shellHookTemplatePath ? ../repo-lib/shell-hook.sh, shellHookTemplatePath ? ../repo-lib/shell-hook.sh,
}: }:
@@ -9,7 +9,7 @@ import ../repo-lib/lib.nix {
inherit inherit
nixpkgs nixpkgs
treefmt-nix treefmt-nix
git-hooks lefthookNix
releaseScriptPath releaseScriptPath
shellHookTemplatePath shellHookTemplatePath
; ;

View File

@@ -1,7 +1,7 @@
{ {
nixpkgs, nixpkgs,
treefmt-nix, treefmt-nix,
git-hooks, lefthookNix,
releaseScriptPath, releaseScriptPath,
shellHookTemplatePath, shellHookTemplatePath,
}: }:
@@ -123,7 +123,7 @@ let
required = tool.required or false; required = tool.required or false;
}; };
normalizeCheck = checkToLefthookConfig =
pkgs: name: rawCheck: pkgs: name: rawCheck:
let let
check = { check = {
@@ -152,13 +152,85 @@ let
then then
throw "repo-lib: check '${name}' has unsupported stage '${check.stage}'" throw "repo-lib: check '${name}' has unsupported stage '${check.stage}'"
else else
{ lib.setAttrByPath [ check.stage "commands" name ] {
enable = true; run = "${wrapper}/bin/${wrapperName}${hookStageFileArgs check.stage check.passFilenames}";
entry = "${wrapper}/bin/${wrapperName}";
pass_filenames = check.passFilenames;
stages = [ check.stage ];
}; };
normalizeLefthookConfig =
label: raw: if builtins.isAttrs raw then raw else throw "repo-lib: ${label} must be an attrset";
normalizeHookStage =
hookName: stage:
if
builtins.elem stage [
"pre-commit"
"pre-push"
"commit-msg"
]
then
stage
else
throw "repo-lib: hook '${hookName}' has unsupported stage '${stage}' for lefthook";
hookStageFileArgs =
stage: passFilenames:
if !passFilenames then
""
else if stage == "pre-commit" then
" {staged_files}"
else if stage == "pre-push" then
" {push_files}"
else if stage == "commit-msg" then
" {1}"
else
throw "repo-lib: unsupported lefthook stage '${stage}'";
hookToLefthookConfig =
name: hook:
let
supportedFields = [
"description"
"enable"
"entry"
"name"
"package"
"pass_filenames"
"stages"
];
unsupportedFields = builtins.filter (field: !(builtins.elem field supportedFields)) (
builtins.attrNames hook
);
stages = builtins.map (stage: normalizeHookStage name stage) (hook.stages or [ "pre-commit" ]);
passFilenames = hook.pass_filenames or false;
in
if unsupportedFields != [ ] then
throw ''
repo-lib: hook '${name}' uses unsupported fields for lefthook: ${lib.concatStringsSep ", " unsupportedFields}
''
else if !(hook ? entry) then
throw "repo-lib: hook '${name}' is missing 'entry'"
else
lib.foldl' lib.recursiveUpdate { } (
builtins.map (
stage:
lib.setAttrByPath [ stage "commands" name ] {
run = "${hook.entry}${hookStageFileArgs stage passFilenames}";
}
) stages
);
parallelHookStageConfig =
stage:
if
builtins.elem stage [
"pre-commit"
"pre-push"
]
then
lib.setAttrByPath [ stage "parallel" ] true
else
{ };
normalizeReleaseStep = normalizeReleaseStep =
step: step:
if step ? writeFile then if step ? writeFile then
@@ -277,7 +349,7 @@ let
buildShellHook = buildShellHook =
{ {
preCommitShellHook, hooksShellHook,
shellEnvScript, shellEnvScript,
bootstrap, bootstrap,
shellBannerScript, shellBannerScript,
@@ -289,7 +361,7 @@ let
in in
builtins.replaceStrings builtins.replaceStrings
[ [
"\${pre-commit-check.shellHook}" "@HOOKS_SHELL_HOOK@"
"@TOOL_LABEL_WIDTH@" "@TOOL_LABEL_WIDTH@"
"@SHELL_ENV_SCRIPT@" "@SHELL_ENV_SCRIPT@"
"@BOOTSTRAP@" "@BOOTSTRAP@"
@@ -297,7 +369,7 @@ let
"@EXTRA_SHELL_TEXT@" "@EXTRA_SHELL_TEXT@"
] ]
[ [
preCommitShellHook hooksShellHook
(toString toolLabelWidth) (toString toolLabelWidth)
shellEnvScript shellEnvScript
bootstrap bootstrap
@@ -322,6 +394,7 @@ let
}, },
checkSpecs ? { }, checkSpecs ? { },
rawHookEntries ? { }, rawHookEntries ? { },
lefthookConfig ? { },
extraPackages ? [ ], extraPackages ? [ ],
}: }:
let let
@@ -343,25 +416,31 @@ let
settings.formatter = { } // formatting.settings; settings.formatter = { } // formatting.settings;
}; };
normalizedChecks = lib.mapAttrs (name: check: normalizeCheck pkgs name check) checkSpecs; normalizedLefthookConfig = normalizeLefthookConfig "lefthook config" lefthookConfig;
hooks = mergeUniqueAttrs "hook" rawHookEntries normalizedChecks; lefthookCheck = lefthookNix.lib.${system}.run {
pre-commit-check = git-hooks.lib.${system}.run {
inherit src; inherit src;
hooks = { config = lib.foldl' lib.recursiveUpdate { } (
treefmt = { [
enable = true; (parallelHookStageConfig "pre-commit")
entry = "${treefmtEval.config.build.wrapper}/bin/treefmt --ci"; (parallelHookStageConfig "pre-push")
pass_filenames = true; (lib.setAttrByPath [ "pre-commit" "commands" "treefmt" ] {
run = "${treefmtEval.config.build.wrapper}/bin/treefmt --ci {staged_files}";
})
(lib.setAttrByPath [ "pre-commit" "commands" "gitleaks" ] {
run = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
})
(lib.setAttrByPath [ "commit-msg" "commands" "gitlint" ] {
run = "${pkgs.gitlint}/bin/gitlint --staged --msg-filename {1}";
})
]
++ lib.mapAttrsToList (name: check: checkToLefthookConfig pkgs name check) checkSpecs
++ lib.mapAttrsToList hookToLefthookConfig rawHookEntries
++ [ normalizedLefthookConfig ]
);
}; };
gitlint.enable = true; selectedCheckOutputs = {
gitleaks = { hook-check = lefthookCheck;
enable = true; lefthook-check = lefthookCheck;
entry = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
pass_filenames = false;
};
}
// hooks;
}; };
toolNames = builtins.map (tool: tool.name) tools; toolNames = builtins.map (tool: tool.name) tools;
@@ -469,19 +548,21 @@ let
''; '';
in in
{ {
inherit pre-commit-check; checks = selectedCheckOutputs;
formatter = treefmtEval.config.build.wrapper; formatter = treefmtEval.config.build.wrapper;
shell = pkgs.mkShell { shell = pkgs.mkShell {
packages = lib.unique (selectedStandardPackages ++ extraPackages ++ toolPackages); packages = lib.unique (
buildInputs = pre-commit-check.enabledPackages; selectedStandardPackages ++ extraPackages ++ toolPackages ++ [ pkgs.lefthook ]
);
shellHook = buildShellHook { shellHook = buildShellHook {
preCommitShellHook = pre-commit-check.shellHook; hooksShellHook = lefthookCheck.shellHook;
inherit toolLabelWidth shellEnvScript shellBannerScript; inherit toolLabelWidth shellEnvScript shellBannerScript;
bootstrap = shellConfig.bootstrap; bootstrap = shellConfig.bootstrap;
extraShellText = shellConfig.extraShellText; extraShellText = shellConfig.extraShellText;
}; };
}; };
}; }
// selectedCheckOutputs;
in in
rec { rec {
systems = { systems = {
@@ -552,6 +633,7 @@ rec {
settings = { }; settings = { };
}; };
checks = { }; checks = { };
lefthook = { };
release = null; release = null;
} rawConfig; } rawConfig;
release = release =
@@ -586,6 +668,7 @@ rec {
preToolHook ? "", preToolHook ? "",
extraShellHook ? "", extraShellHook ? "",
additionalHooks ? { }, additionalHooks ? { },
lefthook ? { },
tools ? [ ], tools ? [ ],
includeStandardPackages ? true, includeStandardPackages ? true,
formatters ? { }, formatters ? { },
@@ -625,6 +708,7 @@ rec {
; ;
formatting = normalizedFormatting; formatting = normalizedFormatting;
rawHookEntries = additionalHooks; rawHookEntries = additionalHooks;
lefthookConfig = lefthook;
shellConfig = shellConfig; shellConfig = shellConfig;
tools = legacyTools; tools = legacyTools;
extraPackages = extraPackages =
@@ -704,6 +788,7 @@ rec {
tools = [ ]; tools = [ ];
shell = { }; shell = { };
checks = { }; checks = { };
lefthook = { };
packages = { }; packages = { };
apps = { }; apps = { };
} }
@@ -716,6 +801,9 @@ rec {
strictTools = builtins.map (tool: normalizeStrictTool pkgs tool) perSystemResult.tools; strictTools = builtins.map (tool: normalizeStrictTool pkgs tool) perSystemResult.tools;
duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) strictTools); duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) strictTools);
mergedChecks = mergeUniqueAttrs "check" normalizedConfig.checks perSystemResult.checks; 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 { }); shellConfig = lib.recursiveUpdate normalizedConfig.shell (perSystemResult.shell or { });
env = env =
if duplicateToolNames != [ ] then if duplicateToolNames != [ ] then
@@ -731,6 +819,7 @@ rec {
formatting = normalizedConfig.formatting; formatting = normalizedConfig.formatting;
tools = strictTools; tools = strictTools;
checkSpecs = mergedChecks; checkSpecs = mergedChecks;
lefthookConfig = mergedLefthookConfig;
shellConfig = shellConfig; shellConfig = shellConfig;
extraPackages = perSystemResult.shell.packages or [ ]; extraPackages = perSystemResult.shell.packages or [ ];
}; };
@@ -762,9 +851,7 @@ rec {
default = systemResults.${system}.env.shell; default = systemResults.${system}.env.shell;
}); });
checks = lib.genAttrs systems (system: { checks = lib.genAttrs systems (system: systemResults.${system}.env.checks);
inherit (systemResults.${system}.env) pre-commit-check;
});
formatter = lib.genAttrs systems (system: systemResults.${system}.env.formatter); formatter = lib.genAttrs systems (system: systemResults.${system}.env.formatter);
packages = lib.genAttrs systems (system: systemResults.${system}.packages); packages = lib.genAttrs systems (system: systemResults.${system}.packages);

View File

@@ -1,4 +1,4 @@
${pre-commit-check.shellHook} @HOOKS_SHELL_HOOK@
if [ -t 1 ]; then if [ -t 1 ]; then
command -v tput >/dev/null 2>&1 && tput clear || printf '\033c' command -v tput >/dev/null 2>&1 && tput clear || printf '\033c'

View File

@@ -35,6 +35,7 @@ repo-lib.lib.mkRepo {
}; };
checks = { }; checks = { };
lefthook = { };
release = null; # or attrset below release = null; # or attrset below
}; };
@@ -43,6 +44,7 @@ repo-lib.lib.mkRepo {
tools = [ ]; tools = [ ];
shell.packages = [ ]; shell.packages = [ ];
checks = { }; checks = { };
lefthook = { };
packages = { }; packages = { };
apps = { }; apps = { };
}; };
@@ -52,7 +54,8 @@ repo-lib.lib.mkRepo {
Generated outputs: Generated outputs:
- `devShells.${system}.default` - `devShells.${system}.default`
- `checks.${system}.pre-commit-check` - `checks.${system}.hook-check`
- `checks.${system}.lefthook-check`
- `formatter.${system}` - `formatter.${system}`
- `packages.${system}.release` when `config.release != null` - `packages.${system}.release` when `config.release != null`
- merged `packages` and `apps` from `perSystem` - merged `packages` and `apps` from `perSystem`
@@ -112,7 +115,36 @@ Defaults:
Rules: Rules:
- Only `pre-commit` and `pre-push` are supported. - Only `pre-commit` and `pre-push` are supported.
- The command is wrapped as a script and connected into `git-hooks.nix`. - 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 ## Tools
@@ -268,6 +300,7 @@ If the user asks for a webhook after the tag exists remotely:
- `extraPackages` - `extraPackages`
- `preToolHook` - `preToolHook`
- `extraShellHook` - `extraShellHook`
- `lefthook`
- `additionalHooks` - `additionalHooks`
- old `tools = [ { name; bin; versionCmd; color; } ]` - old `tools = [ { name; bin; versionCmd; color; } ]`
- `features.oxfmt` - `features.oxfmt`

1
template/.gitignore vendored
View File

@@ -1,5 +1,6 @@
.direnv/ .direnv/
.pre-commit-config.yaml .pre-commit-config.yaml
lefthook.yml
bazel-* bazel-*
build/ build/

View File

@@ -4,7 +4,7 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.1.0"; repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.3.0";
repo-lib.inputs.nixpkgs.follows = "nixpkgs"; repo-lib.inputs.nixpkgs.follows = "nixpkgs";
}; };
@@ -40,6 +40,7 @@
}; };
formatting = { formatting = {
# nixfmt is enabled by default and wired into lefthook.
programs = { programs = {
# shfmt.enable = true; # shfmt.enable = true;
# gofmt.enable = true; # gofmt.enable = true;
@@ -50,14 +51,39 @@
}; };
}; };
checks.tests = { # These checks become lefthook commands in the generated `lefthook.yml`.
# repo-lib runs `pre-commit` and `pre-push` hook commands in parallel.
checks = {
tests = {
command = "echo 'No tests defined yet.'"; command = "echo 'No tests defined yet.'";
stage = "pre-push"; stage = "pre-push";
passFilenames = false; passFilenames = false;
}; };
# fmt = {
# command = "nix fmt";
# stage = "pre-commit";
# passFilenames = false;
# };
};
# 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`
# - gitlint on `commit-msg`
# release = null;
release = { release = {
steps = [ steps = [
# Write a generated version file during release.
# { # {
# writeFile = { # writeFile = {
# path = "src/version.ts"; # path = "src/version.ts";
@@ -66,6 +92,8 @@
# ''; # '';
# }; # };
# } # }
# Replace a version string while preserving surrounding captures.
# { # {
# replace = { # replace = {
# path = "README.md"; # path = "README.md";
@@ -73,6 +101,16 @@
# replacement = ''\1$FULL_VERSION\2''; # replacement = ''\1$FULL_VERSION\2'';
# }; # };
# } # }
# Run any extra release step with declared runtime inputs.
# {
# run = {
# runtimeInputs = [ pkgs.git ];
# script = ''
# git status --short
# '';
# };
# }
]; ];
}; };
}; };
@@ -113,9 +151,16 @@
]; ];
# checks.lint = { # checks.lint = {
# command = "go test ./..."; # command = "bun test";
# stage = "pre-push"; # stage = "pre-push";
# runtimeInputs = [ pkgs.go ]; # passFilenames = false;
# runtimeInputs = [ pkgs.bun ];
# };
# checks.generated = {
# command = "git diff --exit-code";
# stage = "pre-commit";
# passFilenames = false;
# }; # };
# packages.my-tool = pkgs.writeShellApplication { # packages.my-tool = pkgs.writeShellApplication {

View File

@@ -263,6 +263,39 @@ write_mk_repo_command_tool_flake() {
EOF 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
}
write_tool_failure_flake() { write_tool_failure_flake() {
local repo_dir="$1" local repo_dir="$1"
cat >"$repo_dir/flake.nix" <<EOF cat >"$repo_dir/flake.nix" <<EOF
@@ -415,7 +448,7 @@ write_legacy_flake() {
}; };
in in
{ {
inherit (env) pre-commit-check; inherit (env) lefthook-check;
} }
); );
@@ -868,6 +901,38 @@ run_set_prerelease_then_full_case() {
echo "[test] PASS: $case_name" >&2 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() { run_set_stable_then_full_noop_case() {
local case_name="set stable then full fails with no-op" local case_name="set stable then full fails with no-op"
@@ -1124,7 +1189,7 @@ run_mk_repo_case() {
CURRENT_LOG="$workdir/mk-repo.log" CURRENT_LOG="$workdir/mk-repo.log"
run_capture_ok "$case_name: flake show failed" nix flake show --json --no-write-lock-file "$repo_dir" run_capture_ok "$case_name: flake show failed" nix flake show --json --no-write-lock-file "$repo_dir"
assert_contains '"pre-commit-check"' "$CURRENT_LOG" "$case_name: missing pre-commit-check" assert_contains '"lefthook-check"' "$CURRENT_LOG" "$case_name: missing lefthook-check"
assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package" assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package"
assert_contains '"example"' "$CURRENT_LOG" "$case_name: missing merged package" assert_contains '"example"' "$CURRENT_LOG" "$case_name: missing merged package"
@@ -1146,7 +1211,7 @@ run_mk_repo_command_tool_case() {
CURRENT_LOG="$workdir/mk-repo-command-tool.log" CURRENT_LOG="$workdir/mk-repo-command-tool.log"
run_capture_ok "$case_name: flake show failed" nix flake show --json --no-write-lock-file "$repo_dir" run_capture_ok "$case_name: flake show failed" nix flake show --json --no-write-lock-file "$repo_dir"
assert_contains '"pre-commit-check"' "$CURRENT_LOG" "$case_name: missing pre-commit-check" assert_contains '"lefthook-check"' "$CURRENT_LOG" "$case_name: missing lefthook-check"
assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package" assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package"
run_capture_ok "$case_name: system nix should be available in shell" bash -c 'cd "$1" && nix develop --no-write-lock-file . -c nix --version' _ "$repo_dir" run_capture_ok "$case_name: system nix should be available in shell" bash -c 'cd "$1" && nix develop --no-write-lock-file . -c nix --version' _ "$repo_dir"
@@ -1158,6 +1223,38 @@ run_mk_repo_command_tool_case() {
echo "[test] PASS: $case_name" >&2 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() { run_mk_repo_tool_failure_case() {
local case_name="mkRepo required tools fail shell startup" local case_name="mkRepo required tools fail shell startup"
local workdir local workdir
@@ -1202,7 +1299,7 @@ run_legacy_api_eval_case() {
CURRENT_LOG="$workdir/legacy.log" CURRENT_LOG="$workdir/legacy.log"
run_capture_ok "$case_name: flake show failed" nix flake show --json "$repo_dir" run_capture_ok "$case_name: flake show failed" nix flake show --json "$repo_dir"
assert_contains '"pre-commit-check"' "$CURRENT_LOG" "$case_name: missing pre-commit-check" assert_contains '"lefthook-check"' "$CURRENT_LOG" "$case_name: missing lefthook-check"
assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package" assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package"
rm -rf "$workdir" rm -rf "$workdir"
@@ -1220,7 +1317,7 @@ run_template_eval_case() {
CURRENT_LOG="$workdir/template.log" CURRENT_LOG="$workdir/template.log"
run_capture_ok "$case_name: flake show failed" nix flake show --json "$repo_dir" run_capture_ok "$case_name: flake show failed" nix flake show --json "$repo_dir"
assert_contains '"pre-commit-check"' "$CURRENT_LOG" "$case_name: missing pre-commit-check" assert_contains '"lefthook-check"' "$CURRENT_LOG" "$case_name: missing lefthook-check"
assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package" assert_contains '"release"' "$CURRENT_LOG" "$case_name: missing release package"
rm -rf "$workdir" rm -rf "$workdir"
@@ -1264,6 +1361,7 @@ EOF
run_case "channel-only from stable bumps patch" "beta" "1.0.1-beta.1" 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_case "explicit minor bump keeps requested bump" "minor beta" "1.1.0-beta.1"
run_set_prerelease_then_full_case 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_then_full_noop_case
run_set_stable_from_prerelease_requires_full_case run_set_stable_from_prerelease_requires_full_case
run_patch_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_version_metadata_case
run_mk_repo_case run_mk_repo_case
run_mk_repo_command_tool_case run_mk_repo_command_tool_case
run_mk_repo_lefthook_case
run_mk_repo_tool_failure_case run_mk_repo_tool_failure_case
run_impure_bootstrap_validation_case run_impure_bootstrap_validation_case
run_legacy_api_eval_case run_legacy_api_eval_case
run_template_eval_case run_template_eval_case
run_release_replace_backref_case run_release_replace_backref_case
if [[ "${QUICKCHECK:-0}" == "1" ]]; then
run_randomized_quickcheck_cases run_randomized_quickcheck_cases
fi
echo "[test] All release tests passed" >&2 echo "[test] All release tests passed" >&2