An equivalent of `lib.hm.dag`, or some other mechanism for ordering, in `nixpkgs.lib`

I’d really like to be able to control the ordering of - at least - lists at merge-time in NixOS modules. I’ve recently seen a post in Help where someone wanted something similar; though I can’t find it again right now.

The module system already contains a string-exclusive type capable of this (strings-with-deps), and home-manager has a more generic hm.lib.dag, but as far as I can tell there is no way to do this with generic lists today.

Often, the order in lists is incidental, but as explained in Does it matter what order things are listed in configuration.nix?, nix lists are ordered and the order does have practical effects. There are edge cases where the module system cannot unfold its usual power simply because an option uses types.list.

Am I overlooking an existing solution? Is there a specific reason an equivalent doesn’t exist in nixpkgs.lib? I find that hm.lib.dag isn’t particularly ergonomic, either, has there been experimentation or thought about something more flexible, even just at an API level?

Click here for more context in case you suspect an XY problem

My motivation is part of a drive to improve our CI infra at $dayjob, I’m trying to fix a lot of limitations we’re running into with GitHub actions.

I think I’ve stumbled upon a pretty nice way to do this with flake-parts through github-actions-nix; assuming something like lib.hm.dag exists.

I.e., my ultimate goal is to allow something like this (in pseudocode):

# org-wide, shared workflows.nix
{
  perSystem.githubActions.workflows.foo = {
      name = "Example workflow";
      on.push.branches = [ "main" ];

      jobs.build = {
        runsOn = "ubuntu-latest";

        steps.foo = [
          {
            name = "foo";
            run = "exec foo";
          }
        ];
      };
    };    
  };
}
# workflows.nix in a project managed by a specific team
{ lib, ... }: {
  # Override upstream workflow's `foo` with the `foo` from unstable
  perSystem.githubActions.workflows.foo.jobs.build.steps = [
    (lib.mkBefore [ "foo" ] {
      name = "bar";
      run = "nix add nixpkgs-unstable#foo"
    })
  ];
}

Obviously hm.lib.dag doesn’t quite apply, but the use case should be obvious; this lets the downstream flake create a workflow that makes a small tweak to the upstream one, without having to take on ownership of the full workflow. Details like the on stanza don’t have to be repeated, and if the upstream workflow changes what a step does the downstream workflow continues to work as intended. There are limitations, but I think this will generally help fix the unmaintainable (let alone auditable) mess of CI scripts we have.

Of course the example is very specific to my use case, but this kind of composition seems generally useful, and I think nixpkgs.lib could use something like it.

Sorry for making use of community goodwill for topics related to my employment, but hey, if nothing else this might indirectly get my employer to start paying some of y’all :wink:

1 Like

There’s nothing that could do this generally. I have a stalled PR for it though: https://github.com/NixOS/nixpkgs/pull/97392

1 Like

What’s wrong with using this for generic values, exactly? Despite the name, you can put any value in text when you call lib.textClosureList { foo = { deps = [ ]; text = ...; }; }.

1 Like

Huh. Well, the name is an issue for code readability reasons, but I might give that a shot for the time being.