From 80689596c16cb74fd1ec03430a28f7d99f03c0b4 Mon Sep 17 00:00:00 2001 From: poach3r Date: Mon, 14 Sep 2026 20:38:28 -0400 Subject: [PATCH] tangled gen --- README.md | 30 ++++++++++++++++++++++ lib/default.nix | 51 ++++++++++++++++++++++++++++++++----- lib/tangled-gen.nix | 62 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 lib/tangled-gen.nix diff --git a/README.md b/README.md index 3372602..b6dfa68 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,36 @@ hooks. = { `` must match `[A-Za-z0-9_.-]+` +## Tangled pipelines +`mkHooks` can also generate a [Tangled](https://tangled.org) Spindle pipeline: + +```nix +nixhooks.mkHooks { + hooks = { /* ... */ }; + tangled = { + enable = true; + attr = "run-hooks"; + flake = true; + engine = "microvm"; + when = [ + { + event = ["push" "pull_request"]; + branch = ["main"]; + } + ]; + }; +} +``` + +```console +$ nix-build -A gen-tangled-pipeline && ./result/bin/gen-tangled-pipeline +gen-tangled-pipeline: wrote .tangled/workflows/hooks.yml +``` + +This writes a `.tangled/workflows/hooks.yml` in the same manner as +`install-hooks` which you commit to the repo. Note that this isn't wired into +`shellHook` like `install-hooks`, so you'll need to trigger it manually. + # Benchmarks ## Evaluation | | nixhooks | git-hooks.nix | ratio | diff --git a/lib/default.nix b/lib/default.nix index 8c3ccc0..7b3a1c7 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -3,6 +3,7 @@ hookSpec = import ./hook-spec.nix {inherit lib;}; scriptGen = import ./script-gen.nix {inherit lib;}; + tangledGen = import ./tangled-gen.nix {inherit lib;}; runtimeLib = builtins.readFile ./hooks-runtime.sh; driverPreCommit = builtins.readFile ./drivers/pre-commit.sh; @@ -34,8 +35,13 @@ }; # hooks: attrset of name -> hook config, see lib/hook-spec.nix for fields. - mkHooks = {hooks}: let + # tangled: optional Tangled pipeline config, see lib/tangled-gen.nix for fields. + mkHooks = { + hooks, + tangled ? {}, + }: let normalized = hookSpec.normalizeHooks hooks; + tangledCfg = tangledGen.normalizeTangled tangled; preCommitHook = mkStageScript "pre-commit-hook" driverPreCommit ( scriptGen.genStageCalls "pre-commit" normalized @@ -72,13 +78,44 @@ install_one "pre-push" "${prePushHook}/bin/pre-push-hook" ''; }; - in { - pre-commit-hook = preCommitHook; - pre-push-hook = prePushHook; - run-hooks = runHooks; - install-hooks = installHooks; - }; + + tangledPipeline = pkgs.writeText "hooks.yml" '' + ${marker} + ${tangledGen.mkPipelineText tangled} + ''; + + genTangledPipeline = pkgs.writeShellApplication { + name = "gen-tangled-pipeline"; + text = '' + if ! git_root=$(git rev-parse --show-toplevel 2>/dev/null); then + echo "gen-tangled-pipeline: not inside a git repository" >&2 + exit 1 + fi + + dest="$git_root/.tangled/workflows/hooks.yml" + if [[ -e "$dest" ]] && ! grep -q ${lib.escapeShellArg marker} "$dest" 2>/dev/null; then + echo "gen-tangled-pipeline: $dest already exists and is not managed by nixhooks, refusing to overwrite" >&2 + exit 1 + fi + + mkdir -p "$(dirname "$dest")" + install -m 0644 ${tangledPipeline} "$dest" + echo "gen-tangled-pipeline: wrote $dest" + ''; + }; + in + { + pre-commit-hook = preCommitHook; + pre-push-hook = prePushHook; + run-hooks = runHooks; + install-hooks = installHooks; + } + // lib.optionalAttrs tangledCfg.enable { + tangled-pipeline = tangledPipeline; + gen-tangled-pipeline = genTangledPipeline; + }; in { inherit mkHooks; inherit (hookSpec) normalizeHooks normalizeHook defaultHook; + inherit (tangledGen) normalizeTangled defaultTangled; } diff --git a/lib/tangled-gen.nix b/lib/tangled-gen.nix new file mode 100644 index 0000000..9fdeb60 --- /dev/null +++ b/lib/tangled-gen.nix @@ -0,0 +1,62 @@ +{lib}: let + defaultTangled = { + enable = false; + attr = "run-hooks"; + flake = true; + when = [ + { + event = ["push" "pull_request"]; + branch = ["main"]; + } + ]; + engine = "microvm"; + image = "nixos"; + }; + + validEngines = ["microvm" "nixery"]; + + normalizeTangled = raw: let + merged = defaultTangled // raw; + in + assert lib.assertMsg (builtins.isBool merged.enable) + "nixhooks: tangled.enable must be a bool"; + assert lib.assertMsg (merged.attr != "") + "nixhooks: tangled.attr must be a non-empty string"; + assert lib.assertMsg (builtins.elem merged.engine validEngines) + "nixhooks: tangled.engine must be one of: ${builtins.concatStringsSep ", " validEngines}"; + assert lib.assertMsg (builtins.isList merged.when && merged.when != []) + "nixhooks: tangled.when must be a non-empty list of trigger conditions"; merged; + + engineFields = cfg: + if cfg.engine == "microvm" + then {image = cfg.image;} + else {}; + + mkManifest = cfg: let + buildCmd = + if cfg.flake + then "nix build .#${cfg.attr} && ./result/bin/${cfg.attr}" + else "nix-build -A ${cfg.attr} && ./result/bin/${cfg.attr}"; + in + { + when = cfg.when; + engine = cfg.engine; + } + // engineFields cfg + // { + steps = [ + { + name = "nixhooks: ${cfg.attr}"; + command = buildCmd; + } + ]; + }; + + # JSON is valid YAML for some fucking reason so we can just write that + mkPipelineText = raw: let + cfg = normalizeTangled raw; + in + builtins.toJSON (mkManifest cfg); +in { + inherit defaultTangled normalizeTangled mkManifest mkPipelineText; +} -- 2.54.0