Caches and secrets batteries
A cache and a secret are data. The host trusts a cache, the flake asks to trust it, and CI pushes to it. A feature declares the secrets it needs, and a file lists them. Batteries write these from sinks. This fleet has one NixOS host and a repo root that writes files:
# Hosts build NixOS; the repo root writes files. Caches and secrets are sinks any aspect adds to.{ yen, lib, inputs, ...}:let b = yen.lib.batteries;in{ yen.kinds.repo = { spawns.host = _: yen.host; sinks = { out.reduce = yen.lib.reduce.merge; files.out = b.files.writer { from = yen.kinds.repo.sinks.files; inherit (inputs) files; inherit lib; pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux; root = inputs.self; }; }; }; yen.kinds.host.sinks.nixos.out = b.nixos.system { from = yen.kinds.host.sinks.nixos; inherit (inputs) nixpkgs; }; yen.repo.main = { }; yen.host.build = { }; yen.flake = { out, ... }: out; yen.aspects.machine = { at = yen.kinds.host; nixos = { boot.loader.grub.device = "nodev"; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; nixpkgs.hostPlatform = "x86_64-linux"; system.stateVersion = "25.05"; }; };}Any aspect adds a cache to the sinks. The CI job that pushes to the cache:
# cachix: a cache is data; the host trusts it, the flake asks to trust it, CI pushes to it.{ yen, ... }:let inherit (yen.lib.batteries) cachix github files;in{ yen.kinds.host.sinks.caches.nixos = cachix.caches { from = yen.kinds.host.sinks.caches; }; # (1) yen.kinds.repo.sinks.caches.out = cachix.flake-config { from = yen.kinds.repo.sinks.caches; }; # (2) yen.aspects.caches = { at = [ yen.kinds.host yen.kinds.repo ]; caches = { name = "nix-community"; publicKey = "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="; }; }; # the CI job that pushes to the cache yen.kinds.repo.sinks.jobs = { reduce = yen.lib.reduce.merge; files = github.workflow { # (3) from = yen.kinds.repo.sinks.jobs; path = ".github/workflows/build.yml"; doc = { name = "build"; on.push = { }; }; header = _: files.comment "build.yml" "Generated."; }; }; yen.aspects.build-job = { at = yen.kinds.repo; jobs.build = { runs-on = "ubuntu-latest"; steps = [ (cachix.action { name = "my-cache"; }) # (4) { run = "nix build .#nixosConfigurations.build.config.system.build.toplevel"; } ]; }; };}A cache is { name; publicKey; url ? "https://<name>.cachix.org"; }. (1) gives the host nix.settings substituters
and trusted keys. (2) gives the flake nixConfig extra-substituters and keys. At (3), github.workflow writes
the workflow from a sink of jobs, as on Repo batteries. (4) is a workflow step: cachix/cachix-action,
which pushes what the job builds. This is the nixConfig of the flake:
{ extra-substituters = [ "https://nix-community.cachix.org" ]; extra-trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ];}Secrets are declared by the features that need them. The values live in a provider, never in git:
# secretspec.toml: each feature declares the secrets it needs (values live in a provider, never in git).# .sops.yaml: each environment of secrets adds its creation rule.{ yen, ... }:let inherit (yen.lib.batteries) secretspec secrets claim;in{ yen.kinds.repo.sinks = { secretspec = { reduce = claim { key = s: "${s.profile or "default"}/${s.name}"; }; # (1) one secret, one declaration files = secretspec.toml { # (2) from = yen.kinds.repo.sinks.secretspec; project.name = "my-app"; defaults.providers = [ "keyring" ]; }; }; sops-rules.files = secrets.sops-config { from = yen.kinds.repo.sinks.sops-rules; }; # (3) }; yen.aspects.database = { at = yen.kinds.repo; secretspec = { name = "DATABASE_URL"; description = "PostgreSQL connection string"; }; sops-rules = { path_regex = "secrets/db/.*"; age = "age1db"; }; }; yen.aspects.database-dev = { at = yen.kinds.repo; secretspec = { name = "DATABASE_URL"; profile = "development"; required = false; default = "sqlite://./dev.db"; }; };}A secret is { name; profile ? "default"; … } with any secretspec field.
At (1), claim is the reduce: a secret that two scopes declare is an error.
(2) writes secretspec.toml. (3) writes .sops.yaml from a sink of creation rules. These are the files:
[defaults]providers = ["keyring"]
[profiles.default]DATABASE_URL = { description = "PostgreSQL connection string" }
[profiles.development]DATABASE_URL = { default = "sqlite://./dev.db", required = false }
[project]name = "my-app""creation_rules": - "age": "age1db" "path_regex": "secrets/db/.*"The batteries
Section titled “The batteries”| Battery | Value of | Gives |
|---|---|---|
cachix.caches { from; } |
a NixOS, nix-darwin or home-manager key of a sink of caches | nix.settings substituters and trusted keys |
cachix.flake-config { from; } |
a flake-level key | flake nixConfig extra-substituters and keys |
cachix.action { name; authToken; version; } |
a workflow step | cachix/cachix-action, which pushes what the job builds |
secretspec.toml { from; project; defaults; providers; profiles; } |
a key that gives to the files sink | secretspec.toml |
secrets.sops-config { from; path; } |
the same | .sops.yaml from a sink of creation rules |