diff --git a/hosts/common/global/auto-upgrade.nix b/hosts/common/global/auto-upgrade.nix new file mode 100644 index 0000000..ccf295c --- /dev/null +++ b/hosts/common/global/auto-upgrade.nix @@ -0,0 +1,16 @@ +{ + inputs, + config, + ... +}: { + system.hydraAutoUpgrade = { + # Only enable if not dirty + enable = inputs.self ? rev; + dates = "*:0/10"; # Every 10 minutes + instance = "http://hydra.julian-mutter.de"; + project = "dotfiles"; + jobset = "main"; + job = "hosts.${config.networking.hostName}"; + oldFlakeRef = "self"; + }; +} diff --git a/modules/nixos/hydra-auto-upgrade.nix b/modules/nixos/hydra-auto-upgrade.nix new file mode 100644 index 0000000..7faee75 --- /dev/null +++ b/modules/nixos/hydra-auto-upgrade.nix @@ -0,0 +1,129 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.system.hydraAutoUpgrade; +in { + # Taken from Misterio + options = { + system.hydraAutoUpgrade = { + enable = lib.mkEnableOption "periodic hydra-based auto upgrade"; + operation = lib.mkOption { + type = lib.types.enum ["switch" "boot"]; + default = "switch"; + }; + dates = lib.mkOption { + type = lib.types.str; + default = "04:40"; + example = "daily"; + }; + + instance = lib.mkOption { + type = lib.types.str; + example = "http://hydra.julian-mutter.de"; + }; + project = lib.mkOption { + type = lib.types.str; + example = "dotfiles"; + }; + jobset = lib.mkOption { + type = lib.types.str; + example = "main"; + }; + job = lib.mkOption { + type = lib.types.str; + default = config.networking.hostName; + }; + + oldFlakeRef = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Current system's flake reference + + If non-null, the service will only upgrade if the new config is newer + than this one's. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.enable -> !config.system.autoUpgrade.enable; + message = '' + hydraAutoUpgrade and autoUpgrade are mutually exclusive. + ''; + } + ]; + systemd.services.nixos-upgrade = { + description = "NixOS Upgrade"; + restartIfChanged = false; + unitConfig.X-StopOnRemoval = false; + serviceConfig.Type = "oneshot"; + + path = with pkgs; [ + config.nix.package.out + config.programs.ssh.package + coreutils + curl + gitMinimal + gnutar + gzip + jq + nvd + ]; + + script = let + buildUrl = "${cfg.instance}/job/${cfg.project}/${cfg.jobset}/${cfg.job}/latest"; + in + (lib.optionalString (cfg.oldFlakeRef != null) '' + eval="$(curl -sLH 'accept: application/json' "${buildUrl}" | jq -r '.jobsetevals[0]')" + flake="$(curl -sLH 'accept: application/json' "${cfg.instance}/eval/$eval" | jq -r '.flake')" + echo "New flake: $flake" >&2 + new="$(nix flake metadata "$flake" --json | jq -r '.lastModified')" + echo "Modified at: $(date -d @$new)" >&2 + + echo "Current flake: ${cfg.oldFlakeRef}" >&2 + current="$(nix flake metadata "${cfg.oldFlakeRef}" --json | jq -r '.lastModified')" + echo "Modified at: $(date -d @$current)" >&2 + + if [ "$new" -le "$current" ]; then + echo "Skipping upgrade, not newer" >&2 + exit 0 + fi + '') + + '' + profile="/nix/var/nix/profiles/system" + path="$(curl -sLH 'accept: application/json' ${buildUrl} | jq -r '.buildoutputs.out.path')" + + if [ "$(readlink -f "$profile")" = "$path" ]; then + echo "Already up to date" >&2 + exit 0 + fi + + echo "Building $path" >&2 + nix build --no-link "$path" + + echo "Comparing changes" >&2 + nvd --color=always diff "$profile" "$path" + + echo "Activating configuration" >&2 + "$path/bin/switch-to-configuration" test + + echo "Setting profile" >&2 + nix build --no-link --profile "$profile" "$path" + + echo "Adding to bootloader" >&2 + "$path/bin/switch-to-configuration" boot + ''; + + startAt = cfg.dates; + after = ["network-online.target"]; + wants = ["network-online.target"]; + }; + }; +}