Writing a Custom Effect
To write a custom effect, it’s easiest to prototype it in the repository where you want to apply it.
You can then iterate on a template like below.
let
# TODO: Use a recent version
effectsSrc = builtins.fetchTarball "https://github.com/hercules-ci/hercules-ci-effects/archive/b67cfbbb31802389e1fb6a9c75360968d201693b.tar.gz";
# TODO: Use a recent version
nixpkgs = builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/a6a3a368dda.tar.gz";
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [
(import "${effectsSrc}/overlay.nix")
];
};
inherit (pkgs.effects) mkEffect;
runNeatCopy = args@{
hostname,
package,
...
}: mkEffect (args // {
# This style of variable passing allows overrideAttrs and modification in
# hooks like the userSetupScript.
inherit hostname package;
effectScript = ''
nix-copy-closure --use-substitutes --to "$hostname" "$package"
'';
});
in
{
my-neat = runNeatCopy {
hostname = "neathost";
package = pkgs.hello;
};
}
When it works, consider making a pull request to hercules-ci-effects
for the
opportunity of review and improvements.
Integrating a new deployment tool
While it is possible to run Nix evaluations and builds inside the effects sandbox, it is best to build the deployment configuration before running effects. That way you get the most out of Hercules CI: automatic uploading to your cache and if you run multiple effects, it prevents the partial deployment of commits with bad configuration.
Static deployments
Ideally, the deployment tool in your new effect function can be split into two steps; build and deployment.
As a console invocation, this would look like:
# not optimal yet!
$ neat-tool deploy --config $(
nix-build ${neat_tool}/nix/eval-configs.nix \
--arg config ./my-config.nix
)
While you can run nix-build
inside effects, it’s not ideal, because you don’t want to run a single effect when any of their builds fail, and nix-build
doesn’t distribute builds, deduplicate builds, or upload to the cache. Instead, you can replace the $(nix-build …)
subshell expression by an equivalent Nix string interpolation, for example:
effectScript = ''
neat-tool deploy --config ${
import (neat-tool + "/nix/eval-configs.nix") {
config = ./my-config.nix;
}
}
'';
This makes the configuration part of the effect’s closure, so it will be built and cached beforehand.
Dynamic input
If your deployment tool does depend on input that is not statically known, your options depend on how this dynamic information is used.
When you need to use it in a NixOS option for example, you’re usually required to evaluate inside the effect. NixOps is an example of this, because IP addresses and resource attributes aren’t statically known.
A notable exception is where you can provide a static file path that won’t be read by the Nix evaluator, as is the case with "secret" or "key" files.
If you do need to evaluate inside an effect, you may still be able to pre-build with dummy values, so that almost all of your deployment is still built and cached before you run your effects. runNixOps
is an example of this.
NixOS
A plain NixOS deployment is characterized by its toplevel
derivation, which is to be stored in its profile in /nix/var/nix/profiles/system
.
In the simplest case, a deployment tool simply takes this derivation. This is the case with runNixOS
and some simple tools.
The top-level derivation can be retrieved from NixOS' config.system.build.toplevel
attribute. You can create config
by invoking:
-
nixpkgs.lib.nixosSystem { system = x; modules = [ y ]; }
: flakes only -
pkgs.nixos y
: reusingpkgs
, flake or non-flake -
or
import (pkgs.path + "/nixos/lib/eval-config.nix") { system = x; modules = [ y ]; }
: closest to traditionalnixos-rebuild