Repo batteries
A repo has files that should follow the code: the README, the CI workflow, a matrix of CI targets, the dev shell,
flake.nix. Batteries write them from sinks, so any aspect can add a section, a job, a target or an input.
github:sini/files writes them (nix run .#write-files) and gives a check per
file: CI fails when a committed file is stale.
The repo is one root scope that writes repo files, and one scope per system for its dev shell:
# A repo: one root scope that writes repo files, and one scope per system for its dev shell.{ yen, lib, inputs, ...}:let inherit (yen.lib.batteries) files devshell; pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;in{ yen.kinds.repo = { spawns.system = _: yen.system; sinks = { out.reduce = yen.lib.reduce.merge; # every key below that gives to `files` becomes a repo file, written by `nix run .#write-files` files.out = files.writer { # (1) from = yen.kinds.repo.sinks.files; inherit (inputs) files; inherit lib pkgs; root = inputs.self; }; }; }; yen.kinds.system.sinks.shell.out = devshell.mkshell { # (2) from = yen.kinds.system.sinks.shell; pkgsOf = s: inputs.nixpkgs.legacyPackages.${s.name}; }; yen.repo.main = { }; yen.system.x86_64-linux = { }; yen.flake = { out, ... }: out;
yen.aspects.shell-tools = { at = yen.kinds.system; shell.packages = [ ]; shell.shellHook = "echo hello\n"; };}(1) is the files sink of the repo. Every key that gives to it becomes a repo file. files.writer builds
packages.<system>.write-files and a check per file. (2) builds devShells.<scope name>.default from the
{ packages; shellHook; } records of the system scopes.
A README from sections in any file:
# README.md from sections in any file (ordered), with a notice in the file's comment syntax.{ yen, ... }:let inherit (yen.lib.batteries) files ordered;in{ yen.kinds.repo.sinks.readme.reduce = ordered; # (1) yen.aspects.readme = { at = yen.kinds.repo; files = files.document { # (2) from = yen.kinds.repo.sinks.readme; path = "README.md"; }; readme = { name = "title"; value = "# my repo\n${files.comment "README.md" "Generated. Run nix run .#write-files."}"; # (3) }; }; yen.aspects.readme-usage = { at = yen.kinds.repo; readme = { name = "usage"; after = [ "title" ]; value = "## Usage"; }; };}(1) is the ordered reduce: sections come in an order that meets every before and after.
At (2), files.document writes one file from the texts of the sink. (3) is a notice in the comment syntax of the file’s format.
A CI workflow and a matrix of targets:
# CI: jobs from any aspect become .github/workflows/ci.yml; a matrix of targets becomes ci.json.{ yen, ... }:let inherit (yen.lib.batteries) files github;in{ yen.kinds.repo.sinks = { jobs = { reduce = yen.lib.reduce.merge; files = github.workflow { # (1) from = yen.kinds.repo.sinks.jobs; path = ".github/workflows/ci.yml"; doc = { name = "ci"; on.push = { }; }; header = jobs: files.comment "ci.yml" "Generated. Jobs: ${toString jobs}"; # (2) }; }; targets = { reduce = yen.lib.reduce.merge; files = files.json { # (3) from = yen.kinds.repo.sinks.targets; path = "ci.json"; value = builtins.attrValues; }; }; }; yen.aspects.check-job = { at = yen.kinds.repo; jobs.check = { runs-on = "ubuntu-latest"; steps = [ { uses = "actions/checkout@v4"; } { run = "nix flake check"; } ]; }; }; yen.aspects.shell-target = { at = yen.kinds.system; targets = { scope, ... }: { ${scope.name} = "devShells.${scope.name}.default"; }; };}(1) writes a workflow from a sink of jobs, and (2) is its header. (3) writes the JSON of the targets sink to ci.json.
A flake.nix written from the inputs that each aspect declares:
# flake.nix written by github:vic/flake-file: each aspect declares the inputs it uses.{ yen, inputs, ...}:{ yen.kinds.repo.sinks.flake-file = { reduce = yen.lib.reduce.merge; out = yen.lib.batteries.flake-file.write { # (1) from = yen.kinds.repo.sinks.flake-file; inherit (inputs.nixpkgs-unstable) lib; # flake-file needs the unstable lib inherit inputs; pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux; }; }; yen.aspects.flake = { at = yen.kinds.repo; flake-file = { description = "my repo"; outputs = "inputs: import ./outputs.nix inputs"; inputs.nixpkgs.url = "https://nixos.org/channels/nixos-25.05/nixexprs.tar.xz"; }; }; yen.aspects.files-input = { at = yen.kinds.repo; flake-file.inputs.files.url = "github:sini/files"; };}(1) writes flake.nix with flake-file, and gives packages.<system>.write-flake and its check.
More repo files, and a dev shell whose commands come from any aspect:
# More repo files and a numtide/devshell whose commands come from any aspect.{ yen, inputs, ... }:let inherit (yen.lib.batteries) files github devshell;in{ # TOML and YAML from Nix values, as repo files yen.aspects.config-files = { at = yen.kinds.repo; files.files.file = { "taplo.toml".text = files.toml { } { # (1) formatting.align_entries = true; }; ".github/dependabot.yml".text = github.yaml { # (2) version = 2; updates = [ { package-ecosystem = "github-actions"; directory = "/"; schedule.interval = "weekly"; } ]; }; }; }; # each aspect gives a list of commands; the shell shows them in its menu yen.kinds.repo.sinks.commands.out = devshell.numtide { # (3) from = yen.kinds.repo.sinks.commands; inherit (inputs) devshell; systems = [ "x86_64-linux" ]; pkgsOf = s: inputs.nixpkgs.legacyPackages.${s}; name = "repo"; packages = pkgs: [ pkgs.jq ]; shell = "repo"; # the system scope gives `default` }; yen.aspects.write-command = { at = yen.kinds.repo; commands = [ { name = "write"; help = "write the generated files"; command = "nix run .#write-files"; } ]; };}(1) and (2) make TOML and YAML text from Nix values. (3) makes a numtide/devshell for each system.
The files the aspects give, as the files sink holds them:
[ { ".github/dependabot.yml" = '' "updates": - "directory": "/" "package-ecosystem": "github-actions" "schedule": "interval": "weekly" "version": 2 ''; "taplo.toml" = '' [formatting] align_entries = true ''; } { "README.md" = '' # my repo <!-- Generated. Run nix run .#write-files. -->
## Usage''; } { ".github/workflows/ci.yml" = '' # Generated. Jobs: check "jobs": "check": "runs-on": "ubuntu-latest" "steps": - "uses": "actions/checkout@v4" - "run": "nix flake check" "name": "ci" "on": "push": {} ''; } { "ci.json" = '' ["devShells.x86_64-linux.default"] ''; }]The batteries
Section titled “The batteries”| Battery | Value of | Writes |
|---|---|---|
files.writer { from; files; lib; pkgs; root; relativeRoot; } |
out of the files sink |
packages.<system>.write-files and a check per file |
files.document { from; path; separator; } |
a key that gives to the files sink | one file from the texts of a sink (with ordered: sections in order) |
files.json { from; path; value; } |
the same | the JSON of a sink |
files.toml { inline; } |
a function | TOML text from a Nix value |
files.comment path text |
a function | a notice in the comment syntax of the file’s format |
github.workflow { from; path; doc; header; } |
a key that gives to the files sink | a workflow from a sink of jobs |
github.yaml |
a function | YAML text from a Nix value |
devshell.mkshell { from; pkgsOf; } |
out of a sink of { packages; shellHook; } |
devShells.<scope name>.default |
devshell.numtide { from; devshell; systems; pkgsOf; name; packages; } |
out of a sink of command lists |
a numtide/devshell per system |
flake-file.write { from; lib; inputs; pkgs; modules; } |
out of a merge sink |
packages.<system>.write-flake and its check |