1 39b Wrapper Repository Packaging
Nimmo edited this page 2026-09-13 07:09:32 +01:00

Wrapper-Repository Packaging

  • Type: Runbook
  • Status: Current
  • Scope: Packaging an upstream service in an independently maintained flake and consuming it from nixos-config
  • Canonical sources: AGENTS.md; flake.nix; lib/auto-update-policy.nix; scripts/attic-cache-build.sh; modules/services/redview.nix; modules/services/navidrome-mcp.nix; modules/services/forgejo-mcp.nix; .sops.yaml; justfiles/deploy.just; justfiles/secrets.just; ADR-002
  • Last verified: Source commit 556df88494686003b1c4f20c8e0b99b5afc16a6e (2026-09-12); source HEAD b2831d33132967b4c4fb69e6068b2b05fff8fdff changes only flake.lock. Repository contracts checked 2026-09-13; no package was built or deployed for this page.
  • Review triggers: New or removed flake inputs; auto-update policy or controller changes; package/module output changes; SOPS, network, service, CI, or cache contracts

Purpose

Use this procedure when an application is worth operating as a native NixOS service but lacks a suitable package, or its reproducible build and release cycle deserve ownership outside the fleet configuration. ADR-002 owns the placement decision. A wrapper repository owns its upstream pin, package, build checks, and (when reusable) NixOS module. nixos-config owns the pinned input, host integration, secrets, access, state, backup intent, monitoring, and deployment.

Redview is the current full example: its independent flake exports a package, NVIDIA closure, and NixOS module; modules/services/redview.nix imports the module and integrates Lyra's NFS media, secrets, and backup. Navidrome MCP is a smaller independent package consumed by modules/services/navidrome-mcp.nix. The earlier Librarry trial established the pattern but was retired because the application did not fit the fleet. These examples do not imply that every missing nixpkgs package needs a wrapper repository.

Prerequisites and safety boundary

  • Open an issue for the service, with an owner for upstream updates and build failures, an operational-fit assessment, affected hosts, runtime data, clients, and a retirement path. Use a branch and merge request for the cross-repository/service change under AGENTS.md.
  • Check first for a usable nixpkgs package or upstream NixOS module. Read the upstream source, build files, Dockerfile if present, runtime entrypoint, migrations, static assets, required native libraries, and proxy rules. Prove that the software can be built and tested reproducibly before adding a root flake input. A successful package build alone does not prove that the service is safe or useful to operate.
  • Identify authoritative state and schema migration behavior before exposing the new service to real clients. Packaging and cutover are separate changes: follow Service Migration and Cutover when an existing instance or data is involved.
  • Do not put runtime passwords, tokens, or plaintext environment files in the wrapper flake, Nix store, Git, process arguments, or build logs. SOPS files and runtime credential delivery belong at the fleet integration boundary.

Ordered procedure

1. Establish the wrapper contract

Create the dedicated repository only after the buildability review. Pin the upstream source in its flake.lock (or an equivalently immutable source), define clearly named package outputs, and add a NixOS module only when it is reusable outside one host's integration. Keep the derivation and tests in that repository, not under hosts/ in nixos-config. Use nixpkgs.follows in the consumer only when its dependency contract permits it; Redview does not set one, while Navidrome MCP does.

In the wrapper checkout, first inspect output names, then build the exact package or check that the consuming host will use:

nix flake show
nix build .#<package-output> --no-link
nix flake check

Replace <package-output> with a real output. For fixed-output dependency hashes, use the builder's reported mismatch to calculate the correct value, then rebuild; do not accept a placeholder hash or an unbuilt CI workflow. Test the installed executable, assets, migrations, and runtime closure rather than treating derivation success as an end-to-end service test. If the wrapper publishes cache artefacts, verify its own build/push workflow separately; the fleet's Attic workflow does not automatically prove every wrapper output was prewarmed.

2. Add the pinned consumer input and update policy

Add the wrapper to flake.nix and consume its actual output from the relevant module. Existing patterns are inputs.redview.nixosModules.default plus inputs.redview.packages.${pkgs.stdenv.hostPlatform.system}.redview-nvidia, and inputs.navidrome-mcp.packages.${pkgs.stdenv.hostPlatform.system}.default. makeNixosSystem already passes inputs through specialArgs; no second global plumbing layer is needed.

In the same source-repository change, classify the new root input in lib/auto-update-policy.nix: one group per updating input with the real consumer hosts, or an explicit pinned entry and rationale. Do not copy the legacy instruction to edit scripts/nixos-update-common.sh; that file is provided by the separately pinned nixos-auto-update package, which scripts/attic-cache-build.sh builds and sources. The repository's update regression check compares the policy with the actual root inputs and host inventory. A wrapper's own upstream-tracking workflow is not a substitute for the fleet consumer policy or its guarded host builds.

Update the lock deliberately for the new input, inspect the resulting diff, and do not manually float the deployment input to an unreviewed revision:

nix flake update <input-name>
git diff -- flake.nix flake.lock lib/auto-update-policy.nix

The update command is for an intentional packaging change in a feature branch. Routine fleet auto-update follows the producer/consumer control plane in Auto-Update Incident Response, not an unconditional git push from a wrapper workflow.

3. Integrate the NixOS service

Import the fleet module from the consuming host and place reusable integration in modules/services/. Declare the service user or DynamicUser, state directory, restart policy, dependencies, devices/mount guards, listener, firewall, backup contribution, and failure monitoring. Use the upstream module where it fits rather than duplicating its options. Redview demonstrates a mount guard and nixosConfig.backup.dataPaths; Navidrome MCP demonstrates a Nebula-bound listener, device ordering, LoadCredential, and a monitored systemd unit. These are patterns to adapt, not defaults to paste unchanged.

For secrets, create a service-scoped SOPS file under the correct .sops.yaml recipient rule and consume it at runtime via sops.secrets, a SOPS template, or systemd LoadCredential as appropriate. Name NixOS secret attributes uniquely. Verify both the recipient scope and runtime file ownership; a package must not embed secret values. Use Secret Editing and Recipient Rotation for the current helper commands and recovery rules.

Choose access from the actual client graph. A mesh-only service can bind to nixosConfig.nebula.overlayIp and open only networking.firewall.interfaces."nebula.mesh".allowedTCPPorts; it also needs appropriate mesh-device ordering if the bind or upstream connection depends on Nebula. A container on the same host reaches a mesh IP through its bridge, so a mesh-interface-only firewall rule may not admit it. Do not widen to 0.0.0.0 or a global port merely to conceal that mismatch. Confirm ingress, DNS, and authentication separately.

4. Validate, review, and stage deployment

Stage any new .nix file before flake evaluation, then run the applicable checks from the source checkout:

just check
nix build .#nixosConfigurations.<host>.config.system.build.toplevel --no-link

Replace <host> with each consuming host. just check is nix flake check; the host build verifies the actual service closure. Inspect the required Attic producer and update-policy checks in the merge request as well as the resulting package/version diff before a canary. just test-auto-update runs remediation regressions; it is not the input-policy inventory gate. On an affected NixOS host, use Safe Testing and Deployment, including an update hold for a remote test, just test for current-specialisation-preserving activation, and a real client/health check. A wrapper build or flake evaluation is not evidence that the live service, route, database, or backup works.

Success checks

  • The wrapper builds the exact output the host consumes, and its tests and update/cache ownership are explicit.
  • flake.lock pins that output; lib/auto-update-policy.nix names the input and correct consumers, or records a justified pin; CI and host build pass.
  • The canary has the expected package, runtime credentials, listener, mount, service health, client behavior, monitoring, and verified backup coverage.
  • The issue records deployment and no-op follow-up evidence before a deployment-affecting change is closed.

Rollback and troubleshooting

For a package or module regression, stop promotion, inspect the rejected input/build evidence, and return to the last validated wrapper revision or NixOS generation through the normal review/deploy path. If the new version has migrated writable state, package rollback is not data rollback: restore a compatible service-specific snapshot/dump under the cutover plan. Keep the previous route and data recoverable until the acceptance window ends.

When a build fails, distinguish fixed-output hash mismatch, unsupported architecture, missing runtime asset, broken upstream source pin, and a consumer-only NixOS evaluation failure. When the process starts but clients fail, check its own logs, exact proxy path behavior, bind/firewall interface, mesh readiness, and credential paths before rebuilding the package. In particular, an nginx proxy_pass trailing slash can rewrite a route prefix; compare the actual upstream proxy contract before changing it.

Canonical source map

Concern Source
Placement rationale ADR-002
Root inputs and specialArgs flake.nix
Input-to-consumer update policy lib/auto-update-policy.nix
Producer controller source boundary scripts/attic-cache-build.sh, pinned nixos-auto-update input
Fleet module examples modules/services/redview.nix, modules/services/navidrome-mcp.nix
Deploy and secret helpers justfiles/deploy.just, justfiles/secrets.just
Migration of an existing service Service Migration and Cutover