git.delta.rocks / jrsonnet / refs/commits / 1470de8a447c

difftreelog

feat minimal rollback support

Lach2025-06-18parent: #11412de.patch.diff
in: trunk

20 files changed

modifiedCargo.lockdiffbeforeafterboth
before · Cargo.lock
388 packageslockfile v3
after · Cargo.lock
402 packageslockfile v3
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,12 +15,12 @@
 anyhow = "1.0"
 clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
 clap_complete = "4.5"
-nix = { version = "0.29.0", features = ["fs", "user"] }
+nix = { version = "0.30.1", features = ["fs", "user"] }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-tempfile = "3.10"
-thiserror = "2.0.3"
-tokio = { version = "1.36.0", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
-tokio-util = { version = "0.7.11", features = ["codec"] }
+tempfile = "3.20"
+thiserror = "2.0.12"
+tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
+tokio-util = { version = "0.7.15", features = ["codec"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -5,6 +5,7 @@
 authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
 edition.workspace = true
 rust-version.workspace = true
+default-run = "fleet"
 
 [dependencies]
 age = { workspace = true, features = ["armor"] }
@@ -27,23 +28,23 @@
 async-trait = "0.1"
 base64 = "0.22.1"
 chrono = { version = "0.4", features = ["serde"] }
-crossterm = { version = "0.28.0", features = ["use-dev-tty"] }
+crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
 futures = "0.3"
-hostname = "0.4.0"
-itertools = "0.13"
+hostname = "0.4.1"
+itertools = "0.14"
 openssh = "0.11"
-owo-colors = { version = "4.0", features = ["supports-color", "supports-colors"] }
+owo-colors = { version = "4.2", features = ["supports-color", "supports-colors"] }
 peg = "0.8"
-regex = "1.10"
+regex = "1.11"
 shlex = "1.3"
-tabled = { version = "0.16" }
+tabled = { version = "0.20" }
 time = { version = "0.3", features = ["serde"] }
 tokio-util = { version = "0.7", features = ["codec"] }
 
 fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" }
 human-repr = { version = "1.1", optional = true }
 indicatif = { version = "0.17", optional = true }
-nom = "7.1.3"
+nom = "8.0.0"
 tracing-indicatif = { version = "0.3", optional = true }
 
 [features]
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,14 +1,14 @@
-use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, time::Duration};
+use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
 
-use anyhow::{anyhow, bail, Context, Result};
-use clap::{Parser, ValueEnum};
+use anyhow::{anyhow, Result};
+use clap::Parser;
 use fleet_base::{
-	host::{Config, ConfigHost, DeployKind},
+	deploy::{deploy_task, upload_task, DeployAction},
+	host::{Config, DeployKind, GenerationStorage},
 	opts::FleetOpts,
 };
-use itertools::Itertools as _;
 use nix_eval::{nix_go, NixBuildBatch};
-use tokio::{task::LocalSet, time::sleep};
+use tokio::task::LocalSet;
 use tracing::{error, field, info, info_span, warn, Instrument};
 
 #[derive(Parser)]
@@ -18,300 +18,16 @@
 	disable_rollback: bool,
 	/// Action to execute after system is built
 	action: DeployAction,
-}
-
-#[derive(ValueEnum, Clone, Copy)]
-enum DeployAction {
-	/// Upload derivation, but do not execute the update.
-	Upload,
-	/// Upload and execute the activation script, old version will be used after reboot.
-	Test,
-	/// Upload and set as current system profile, but do not execute activation script.
-	Boot,
-	/// Upload, set current profile, and execute activation script.
-	Switch,
 }
 
-impl DeployAction {
-	pub(crate) fn name(&self) -> Option<&'static str> {
-		match self {
-			Self::Upload => None,
-			Self::Test => Some("test"),
-			Self::Boot => Some("boot"),
-			Self::Switch => Some("switch"),
-		}
-	}
-	pub(crate) fn should_switch_profile(&self) -> bool {
-		matches!(self, Self::Switch | Self::Boot)
-	}
-	pub(crate) fn should_activate(&self) -> bool {
-		matches!(self, Self::Switch | Self::Test | Self::Boot)
-	}
-	pub(crate) fn should_create_rollback_marker(&self) -> bool {
-		// Upload does nothing on the target machine, other than uploading the closure.
-		// In boot case we want to have rollback marker prepared, so that the system may rollback itself on the next boot.
-		!matches!(self, Self::Upload)
-	}
-	pub(crate) fn should_schedule_rollback_run(&self) -> bool {
-		matches!(self, Self::Switch | Self::Test)
-	}
-}
-
 #[derive(Parser, Clone)]
 pub struct BuildSystems {
 	/// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes
 	/// are "sdImage"/"isoImage", and your configuration may include any other build attributes.
 	#[clap(long, default_value = "toplevel")]
 	build_attr: String,
-}
-
-struct Generation {
-	id: u32,
-	current: bool,
-	datetime: String,
-}
-
-fn parse_generation_line(g: &str) -> Option<Generation> {
-	let mut parts = g.split_whitespace();
-	let id = parts.next()?;
-	let id: u32 = id.parse().ok()?;
-	let date = parts.next()?;
-	let time = parts.next()?;
-	let current = if let Some(current) = parts.next() {
-		if current == "(current)" {
-			Some(true)
-		} else {
-			None
-		}
-	} else {
-		Some(false)
-	};
-	let current = current?;
-	if parts.next().is_some() {
-		warn!("unexpected text after generation: {g}");
-	}
-	Some(Generation {
-		id,
-		current,
-		datetime: format!("{date} {time}"),
-	})
-}
-
-async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {
-	let mut cmd = host.cmd("nix-env").await?;
-	cmd.comparg("--profile", "/nix/var/nix/profiles/system")
-		.arg("--list-generations");
-	// Sudo is required due to --list-generations acquiring lock on the profile.
-	let data = cmd.sudo().run_string().await?;
-	let generations = data
-		.split('\n')
-		.map(|e| e.trim())
-		.filter(|&l| !l.is_empty())
-		.filter_map(|g| {
-			let gen = parse_generation_line(g);
-			if gen.is_none() {
-				warn!("bad generation: {g}");
-			}
-			gen
-		})
-		.collect::<Vec<_>>();
-	let current = generations
-		.into_iter()
-		.filter(|g| g.current)
-		.at_most_one()
-		.map_err(|_e| anyhow!("bad list-generations output"))?
-		.ok_or_else(|| anyhow!("failed to find generation"))?;
-	Ok(current)
 }
-
-async fn deploy_task(
-	action: DeployAction,
-	host: &ConfigHost,
-	built: PathBuf,
-	specialisation: Option<String>,
-	disable_rollback: bool,
-) -> Result<()> {
-	let deploy_kind = host.deploy_kind().await?;
-	if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
-		&& !matches!(action, DeployAction::Boot | DeployAction::Upload)
-	{
-		bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");
-	}
-
-	let mut failed = false;
 
-	// TODO: Lockfile, to prevent concurrent system switch?
-	// TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback
-	// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
-	// unit name conflict in systemd-run
-	// This code is tied to rollback.nix
-	if !disable_rollback && action.should_create_rollback_marker() {
-		let _span = info_span!("preparing").entered();
-		info!("preparing for rollback");
-		let generation = get_current_generation(host).await?;
-		info!(
-			"rollback target would be {} {}",
-			generation.id, generation.datetime
-		);
-		{
-			let mut cmd = host.cmd("sh").await?;
-			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
-			if let Err(e) = cmd.sudo().run().await {
-				error!("failed to set rollback marker: {e}");
-				failed = true;
-			}
-		}
-		// Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.
-		// Kicking it on manually will work best.
-		//
-		// There wouldn't be conflict, because here we trigger start of the primary service, and systemd will
-		// only allow one instance of it.
-
-		// TODO: We should also watch how this process is going.
-		// After running this command, we have less than 3 minutes to deploy everything,
-		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
-		// Anyway, reboot will still help in this case.
-		if action.should_schedule_rollback_run() {
-			let mut cmd = host.cmd("systemd-run").await?;
-			cmd.comparg("--on-active", "3min")
-				.comparg("--unit", "rollback-watchdog-run")
-				.arg("systemctl")
-				.arg("start")
-				.arg("rollback-watchdog.service");
-			if let Err(e) = cmd.sudo().run().await {
-				error!("failed to schedule rollback run: {e}");
-				failed = true;
-			}
-		}
-	}
-	if deploy_kind == DeployKind::NixosLustrate {
-		// Fleet could also create this file, but as this operation is potentially disruptive,
-		// make user do it themself.
-		if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
-			bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
-		}
-		// Wanted by NixOS to recognize the system as NixOS.
-		let mut cmd = host.cmd("touch").await?;
-		cmd.arg("/etc/NIXOS");
-		cmd.sudo().run().await.context("creating /etc/NIXOS")?;
-	}
-	if deploy_kind == DeployKind::NixosInstall {
-		info!(
-			"running nixos-install to switch profile, install bootloader, and perform activation"
-		);
-		let mut cmd = host.cmd("nixos-install").await?;
-		cmd.arg("--system").arg(&built).args([
-			// Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
-			// It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
-			"--no-channel-copy",
-			"--root",
-			"/mnt",
-		]);
-		if let Err(e) = cmd.sudo().run().await {
-			error!("failed to execute nixos-install: {e}");
-			failed = true;
-		}
-	} else {
-		if action.should_switch_profile() && !failed {
-			info!("switching system profile generation");
-
-			// To avoid even more problems, using nixos-install for now.
-			// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
-			// // falling back to using nix-env command
-			// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
-			// let mut cmd = host.cmd("nix-env").await?;
-			// cmd.args([
-			// 	"--store",
-			// 	"/mnt",
-			// 	"--profile",
-			// 	"/mnt/nix/var/nix/profiles/system",
-			// 	"--set",
-			// ])
-			// .arg(&built);
-			// if let Err(e) = cmd.sudo().run_nix().await {
-			// 	error!("failed to switch system profile generation: {e}");
-			// 	failed = true;
-			// }
-			// It would also be possible to update profile atomically during copy:
-			// https://github.com/NixOS/nix/pull/11657
-			let mut cmd = host.nix_cmd().await?;
-			cmd.arg("build");
-			cmd.comparg("--profile", "/nix/var/nix/profiles/system");
-			cmd.arg(&built);
-			if let Err(e) = cmd.sudo().run_nix().await {
-				error!("failed to switch system profile generation: {e}");
-				failed = true;
-			}
-		}
-
-		// FIXME: Connection might be disconnected after activation run
-
-		if action.should_activate() && !failed {
-			let _span = info_span!("activating").entered();
-			info!("executing activation script");
-			let specialised = if let Some(specialisation) = specialisation {
-				let mut specialised = built.join("specialisation");
-				specialised.push(specialisation);
-				specialised
-			} else {
-				built.clone()
-			};
-			let switch_script = specialised.join("bin/switch-to-configuration");
-			let mut cmd = host.cmd(switch_script).in_current_span().await?;
-			if deploy_kind == DeployKind::NixosLustrate {
-				cmd.env("NIXOS_INSTALL_BOOTLOADER", "1");
-			}
-			cmd.env("FLEET_ONLINE_ACTIVATION", "1")
-				.arg(action.name().expect("upload.should_activate == false"));
-			if let Err(e) = cmd.sudo().run().in_current_span().await {
-				error!("failed to activate: {e}");
-				failed = true;
-			}
-		}
-	}
-	if action.should_create_rollback_marker() {
-		if !disable_rollback {
-			if failed {
-				if action.should_schedule_rollback_run() {
-					info!("executing rollback");
-					if let Err(e) = host
-						.systemctl_start("rollback-watchdog.service")
-						.instrument(info_span!("rollback"))
-						.await
-					{
-						error!("failed to trigger rollback: {e}")
-					}
-				}
-			} else {
-				info!("trying to mark upgrade as successful");
-				if let Err(e) = host
-					.rm_file("/etc/fleet_rollback_marker", true)
-					.in_current_span()
-					.await
-				{
-					error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
-				}
-			}
-			info!("disarming watchdog, just in case");
-			if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
-				// It is ok, if there was no reboot - then timer might not be running.
-			}
-			if action.should_schedule_rollback_run() {
-				if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
-					error!("failed to disarm rollback run: {e}");
-				}
-			}
-		} else if let Err(_e) = host
-			.rm_file("/etc/fleet_rollback_marker", true)
-			.in_current_span()
-			.await
-		{
-			// Marker might not exist, yet better try to remove it.
-		}
-	}
-	Ok(())
-}
-
 async fn build_task(
 	config: Config,
 	hostname: String,
@@ -328,7 +44,8 @@
 		.get("out")
 		.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;
 
-	{
+	// We already have system profiles for backups.
+	if !host.local {
 		info!("adding gc root");
 		let mut cmd = config.local_host().cmd("nix").await?;
 		cmd.arg("build")
@@ -403,7 +120,6 @@
 			let config = config.clone();
 			let span = info_span!("deploy", host = field::display(&host.name));
 			let hostname = host.name.clone();
-			let local_host = config.local_host();
 			let opts = opts.clone();
 			let batch = batch.clone();
 			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
@@ -437,51 +153,20 @@
 						disable_rollback = true;
 					}
 
-					if !opts.is_local(&hostname) {
-						info!("uploading system closure");
+					let remote_path =
+						match upload_task(&config, &host, GenerationStorage::Deployer, built).await
 						{
-							// TODO: Move to remote_derivation method.
-							// Alternatively, nix store make-content-addressed can be used,
-							// at least for the first deployment, to provide trusted store key.
-							//
-							// It is much slower, yet doesn't require root on the deployer machine.
-							let Ok(mut sign) = local_host.cmd("nix").await else {
-								error!("failed to setup local");
+							Ok(v) => v,
+							Err(e) => {
+								error!("upload failed: {e}");
 								return;
-							};
-							// Private key for host machine is registered in nix-sign.nix
-							sign.arg("store")
-								.arg("sign")
-								.comparg("--key-file", "/etc/nix/private-key")
-								.arg("-r")
-								.arg(&built);
-							if let Err(e) = sign.sudo().run_nix().await {
-								warn!("failed to sign store paths: {e}");
-							};
-						}
-						let mut tries = 0;
-						loop {
-							match host.remote_derivation(&built).await {
-								Ok(remote) => {
-									assert!(remote == built, "CA derivations aren't implemented");
-									break;
-								}
-								Err(e) if tries < 3 => {
-									tries += 1;
-									warn!("copy failure ({}/3): {}", tries, e);
-									sleep(Duration::from_millis(5000)).await;
-								}
-								Err(e) => {
-									error!("upload failed: {e}");
-									return;
-								}
 							}
-						}
-					}
+						};
+
 					if let Err(e) = deploy_task(
 						self.action,
 						&host,
-						built,
+						remote_path,
 						if let Ok(v) = opts.action_attr(&host, "specialisation").await {
 							v
 						} else {
modifiedcmds/fleet/src/cmds/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/mod.rs
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -3,3 +3,4 @@
 pub mod info;
 pub mod secrets;
 pub mod tf;
+pub mod rollback;
\ No newline at end of file
addedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
--- /dev/null
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -0,0 +1,127 @@
+use std::collections::HashSet;
+
+use anyhow::{bail, Result};
+use clap::Parser;
+use fleet_base::{
+	deploy::{deploy_task, upload_task, DeployAction},
+	host::{Config, ConfigHost, Generation, GenerationStorage},
+	opts::FleetOpts,
+};
+use tabled::Table;
+use tracing::{info, warn};
+
+#[derive(Parser)]
+pub struct RollbackSingle {
+	machine: String,
+	#[clap(subcommand)]
+	action: RollbackAction,
+}
+
+#[derive(Parser, Clone)]
+struct DeployOptions {
+	/// Rollback target to use
+	id: String,
+	/// Rollback to the current generation if rollback fails
+	// Automatic rollback seems to be unnecessary for manual rollback...
+	#[clap(long)]
+	enable_rollback: bool,
+	/// Specialization to use
+	#[clap(long)]
+	specialization: Option<String>,
+}
+
+#[derive(Parser, Clone)]
+enum RollbackAction {
+	/// List available rollback targets
+	ListTargets,
+	/// Upload and execute the activation script, old version will be used after reboot.
+	Test(#[clap(flatten)] DeployOptions),
+	/// Upload, set current profile, and execute activation script.
+	Switch(#[clap(flatten)] DeployOptions),
+	/// Upload and set as current system profile, but do not execute activation script.
+	Boot(#[clap(flatten)] DeployOptions),
+}
+
+pub async fn list_all_generations(host: &ConfigHost, config: &Config) -> Vec<Generation> {
+	let stored_on_machine = host
+		.list_generations("system")
+		.await
+		.inspect_err(|e| {
+			warn!("failed to list generations available on the remote machine: {e}");
+		})
+		.unwrap_or_default();
+	let on_machine_store_paths = stored_on_machine
+		.iter()
+		.map(|g| &g.store_path)
+		.collect::<HashSet<_>>();
+	let mut stored_locally = config
+		.local_host()
+		.list_generations(&format!("{}-{}", config.data().gc_root_prefix, host.name))
+		.await
+		.inspect_err(|e| {
+			warn!("failed to list generations available locally: {e}");
+		})
+		.unwrap_or_default();
+	dbg!(&stored_locally);
+	stored_locally.retain(|g| !on_machine_store_paths.contains(&g.store_path));
+	for ele in stored_locally.iter_mut() {
+		ele.current = false;
+		ele.location = GenerationStorage::Deployer;
+	}
+	stored_locally.extend(stored_on_machine);
+	stored_locally.sort_by_key(|v| v.datetime);
+	stored_locally
+}
+
+impl RollbackSingle {
+	pub(crate) async fn run(&self, config: &Config, _opts: &FleetOpts) -> Result<()> {
+		let host = config.host(&self.machine).await?;
+		match &self.action {
+			RollbackAction::ListTargets => {
+				let generations = list_all_generations(&host, config).await;
+				if generations.is_empty() {
+					bail!("no available rollback targets found");
+				}
+				info!("Generation list:\n{}", Table::new(&generations));
+				Ok(())
+			}
+			RollbackAction::Boot(o) | RollbackAction::Test(o) | RollbackAction::Switch(o) => {
+				let DeployOptions {
+					id,
+					enable_rollback,
+					specialization,
+				} = o;
+				let action: DeployAction = match self.action {
+					RollbackAction::Test { .. } => DeployAction::Test,
+					RollbackAction::Switch { .. } => DeployAction::Switch,
+					RollbackAction::Boot { .. } => DeployAction::Boot,
+					_ => unreachable!(),
+				};
+				let generations = list_all_generations(&host, config).await;
+				let Some(generation) = generations.iter().find(|g| &g.rollback_id() == id) else {
+					bail!(
+						"generation by this name is not found, existing generations:\n{}",
+						Table::new(&generations)
+					);
+				};
+				let remote_path = upload_task(
+					config,
+					&host,
+					generation.location,
+					generation.store_path.clone(),
+				)
+				.await?;
+
+				deploy_task(
+					action,
+					&host,
+					remote_path,
+					specialization.clone(),
+					!*enable_rollback,
+				)
+				.await?;
+				Ok(())
+			}
+		}
+	}
+}
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -10,6 +10,7 @@
 use clap::{CommandFactory, Parser};
 use cmds::{
 	build_systems::{BuildSystems, Deploy},
+	rollback::RollbackSingle,
 	complete::Complete,
 	info::Info,
 	secrets::Secret,
@@ -70,6 +71,8 @@
 	BuildSystems(BuildSystems),
 	/// Upload and switch system closures
 	Deploy(Deploy),
+	/// Rollback remote machine by redeploying old generation as the new one
+	RollbackSingle(RollbackSingle),
 	/// Secret management
 	#[clap(subcommand)]
 	Secret(Secret),
@@ -97,6 +100,7 @@
 	match command {
 		Opts::BuildSystems(c) => c.run(config, &opts).await?,
 		Opts::Deploy(d) => d.run(config, &opts).await?,
+		Opts::RollbackSingle(r) => r.run(config, &opts).await?,
 		Opts::Secret(s) => s.run(config, &opts).await?,
 		Opts::Info(i) => i.run(config).await?,
 		Opts::Prefetch(p) => p.run(config).await?,
modifiedcmds/generator-helper/Cargo.tomldiffbeforeafterboth
--- a/cmds/generator-helper/Cargo.toml
+++ b/cmds/generator-helper/Cargo.toml
@@ -11,7 +11,7 @@
 fleet-shared.workspace = true
 
 base64 = "0.22.1"
-ed25519-dalek = { version = "2.1", features = ["rand_core"] }
+ed25519-dalek = { version = "2.1" }
 hex = "0.4.3"
-rand = "0.8.5"
-x25519-dalek = "2.0.1"
+rand = "0.9.1"
+x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -11,10 +11,11 @@
 };
 use anyhow::{anyhow, bail, ensure, Context, Result};
 use clap::{Parser, ValueEnum};
+use ed25519_dalek::SecretKey;
 use fleet_shared::SecretData;
 use rand::{
-	distributions::{Alphanumeric, DistString, Distribution, Uniform},
-	thread_rng, RngCore,
+	distr::{Alphanumeric, Distribution, SampleString, Uniform},
+	rng, RngCore,
 };
 
 fn write_output_file(out: &str) -> Result<File> {
@@ -224,7 +225,7 @@
 fn main() -> Result<()> {
 	let opts = Opts::parse();
 	// Assumed to be secure, seeded from secure OsRng+reseeded.
-	let mut rng = thread_rng();
+	let mut rng = rng();
 
 	match opts {
 		Opts::Public { output, encoding } => {
@@ -245,7 +246,10 @@
 					use ed25519_dalek::SigningKey;
 
 					let recipients = load_identities()?;
-					let key = SigningKey::generate(&mut rng).to_keypair_bytes();
+					let mut secret = SecretKey::default();
+					rng.fill_bytes(&mut secret);
+					// TODO: Use SigningKey::generate after https://github.com/dalek-cryptography/curve25519-dalek/pull/762
+					let key = SigningKey::from_bytes(&secret).to_keypair_bytes();
 					write_public(&public, &key[32..], encoding)?;
 					write_private(
 						&recipients,
@@ -268,7 +272,8 @@
 					use x25519_dalek::{PublicKey, StaticSecret};
 
 					let recipients = load_identities()?;
-					let key = StaticSecret::random_from_rng(rng);
+					// TODO: Use random_from_rng after https://github.com/dalek-cryptography/curve25519-dalek/pull/762
+					let key = StaticSecret::random();
 					let public_key: PublicKey = (&key).into();
 					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;
 					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;
@@ -289,7 +294,8 @@
 					} else {
 						// Alphabet of Alphanumberic + symbols
 						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
-						let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());
+						let uniform =
+							Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");
 						(0..size)
 							.map(|_| uniform.sample(&mut rng))
 							.map(|i| GEN_ASCII_SYMBOLS[i] as char)
@@ -310,7 +316,9 @@
 					let recipients = load_identities()?;
 					let mut bytes = vec![0u8; count];
 					if no_nuls {
-						let rand = Uniform::new_inclusive(0x1u8, 0xffu8).sample_iter(&mut rng);
+						let rand = Uniform::new_inclusive(0x1u8, 0xffu8)
+							.expect("range is valid")
+							.sample_iter(&mut rng);
 						for (byte, rand) in bytes.iter_mut().zip(rand) {
 							*byte = rand;
 						}
modifiedcmds/terraform-provider-fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/terraform-provider-fleet/Cargo.toml
+++ b/cmds/terraform-provider-fleet/Cargo.toml
@@ -9,5 +9,5 @@
 serde = { workspace = true, features = ["derive"] }
 tokio.workspace = true
 
-async-trait = "0.1.81"
+async-trait = "0.1.88"
 tf-provider = "0.2.2"
modifiedcrates/better-command/Cargo.tomldiffbeforeafterboth
--- a/crates/better-command/Cargo.toml
+++ b/crates/better-command/Cargo.toml
@@ -5,7 +5,7 @@
 rust-version.workspace = true
 
 [dependencies]
-regex = "1.10"
+regex = "1.11"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 tracing = "0.1"
modifiedcrates/fleet-base/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-base/Cargo.toml
+++ b/crates/fleet-base/Cargo.toml
@@ -8,21 +8,23 @@
 age.workspace = true
 anyhow.workspace = true
 better-command.workspace = true
-chrono = "0.4.38"
+chrono = "0.4.41"
 clap = { workspace = true, features = ["derive"] }
 fleet-shared.workspace = true
-futures = "0.3.30"
-hostname = "0.4.0"
+futures = "0.3.31"
+hostname = "0.4.1"
 indoc = "2.0.6"
-itertools = "0.13.0"
+itertools = "0.14.0"
 nix-eval.workspace = true
 nixlike.workspace = true
-nom = "7.1.3"
-openssh = "0.11.0"
-rand = "0.8.5"
+nom = "8.0.0"
+openssh = "0.11.5"
+rand = "0.9.1"
 serde.workspace = true
-serde_json = "1.0.127"
+serde_json = "1.0.140"
+tabled = "0.20.0"
 tempfile.workspace = true
+time = { version = "0.3.41", features = ["parsing"] }
 tokio.workspace = true
-tokio-util = "0.7.11"
+tokio-util = "0.7.15"
 tracing.workspace = true
addedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/fleet-base/src/deploy.rs
@@ -0,0 +1,297 @@
+use std::{path::PathBuf, time::Duration};
+
+use anyhow::{anyhow, bail, Context as _, Result};
+use clap::ValueEnum;
+use itertools::Itertools;
+use tokio::time::sleep;
+use tracing::{error, info, info_span, warn, Instrument as _};
+
+use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};
+
+#[derive(ValueEnum, Clone, Copy)]
+pub enum DeployAction {
+	/// Upload derivation, but do not execute the update.
+	Upload,
+	/// Upload and execute the activation script, old version will be used after reboot.
+	Test,
+	/// Upload and set as current system profile, but do not execute activation script.
+	Boot,
+	/// Upload, set current profile, and execute activation script.
+	Switch,
+}
+
+impl DeployAction {
+	pub(crate) fn name(&self) -> Option<&'static str> {
+		match self {
+			Self::Upload => None,
+			Self::Test => Some("test"),
+			Self::Boot => Some("boot"),
+			Self::Switch => Some("switch"),
+		}
+	}
+	pub(crate) fn should_switch_profile(&self) -> bool {
+		matches!(self, Self::Switch | Self::Boot)
+	}
+	pub(crate) fn should_activate(&self) -> bool {
+		matches!(self, Self::Switch | Self::Test | Self::Boot)
+	}
+	pub(crate) fn should_create_rollback_marker(&self) -> bool {
+		// Upload does nothing on the target machine, other than uploading the closure.
+		// In boot case we want to have rollback marker prepared, so that the system may rollback itself on the next boot.
+		!matches!(self, Self::Upload)
+	}
+	pub(crate) fn should_schedule_rollback_run(&self) -> bool {
+		matches!(self, Self::Switch | Self::Test)
+	}
+}
+
+async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {
+	let generations = host.list_generations("system").await?;
+	let current = generations
+		.into_iter()
+		.filter(|g| g.current)
+		.at_most_one()
+		.map_err(|_e| anyhow!("bad list-generations output"))?
+		.ok_or_else(|| anyhow!("failed to find generation"))?;
+	Ok(current)
+}
+
+pub async fn deploy_task(
+	action: DeployAction,
+	host: &ConfigHost,
+	built: PathBuf,
+	specialisation: Option<String>,
+	disable_rollback: bool,
+) -> Result<()> {
+	let deploy_kind = host.deploy_kind().await?;
+	if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
+		&& !matches!(action, DeployAction::Boot | DeployAction::Upload)
+	{
+		bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");
+	}
+
+	let mut failed = false;
+
+	// TODO: Lockfile, to prevent concurrent system switch?
+	// TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback
+	// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
+	// unit name conflict in systemd-run
+	// This code is tied to rollback.nix
+	if !disable_rollback && action.should_create_rollback_marker() {
+		let _span = info_span!("preparing").entered();
+		info!("preparing for rollback");
+		let generation = get_current_generation(host).await?;
+		info!(
+			"rollback target would be {} {}",
+			generation.id, generation.datetime
+		);
+		{
+			let mut cmd = host.cmd("sh").await?;
+			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
+			if let Err(e) = cmd.sudo().run().await {
+				error!("failed to set rollback marker: {e}");
+				failed = true;
+			}
+		}
+		// Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.
+		// Kicking it on manually will work best.
+		//
+		// There wouldn't be conflict, because here we trigger start of the primary service, and systemd will
+		// only allow one instance of it.
+
+		// TODO: We should also watch how this process is going.
+		// After running this command, we have less than 3 minutes to deploy everything,
+		// if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
+		// Anyway, reboot will still help in this case.
+		if action.should_schedule_rollback_run() {
+			let mut cmd = host.cmd("systemd-run").await?;
+			cmd.comparg("--on-active", "3min")
+				.comparg("--unit", "rollback-watchdog-run")
+				.arg("systemctl")
+				.arg("start")
+				.arg("rollback-watchdog.service");
+			if let Err(e) = cmd.sudo().run().await {
+				error!("failed to schedule rollback run: {e}");
+				failed = true;
+			}
+		}
+	}
+	if deploy_kind == DeployKind::NixosLustrate {
+		// Fleet could also create this file, but as this operation is potentially disruptive,
+		// make user do it themself.
+		if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
+			bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
+		}
+		// Wanted by NixOS to recognize the system as NixOS.
+		let mut cmd = host.cmd("touch").await?;
+		cmd.arg("/etc/NIXOS");
+		cmd.sudo().run().await.context("creating /etc/NIXOS")?;
+	}
+	if deploy_kind == DeployKind::NixosInstall {
+		info!(
+			"running nixos-install to switch profile, install bootloader, and perform activation"
+		);
+		let mut cmd = host.cmd("nixos-install").await?;
+		cmd.arg("--system").arg(&built).args([
+			// Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
+			// It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
+			"--no-channel-copy",
+			"--root",
+			"/mnt",
+		]);
+		if let Err(e) = cmd.sudo().run().await {
+			error!("failed to execute nixos-install: {e}");
+			failed = true;
+		}
+	} else {
+		if action.should_switch_profile() && !failed {
+			info!("switching system profile generation");
+
+			// To avoid even more problems, using nixos-install for now.
+			// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
+			// // falling back to using nix-env command
+			// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
+			// let mut cmd = host.cmd("nix-env").await?;
+			// cmd.args([
+			// 	"--store",
+			// 	"/mnt",
+			// 	"--profile",
+			// 	"/mnt/nix/var/nix/profiles/system",
+			// 	"--set",
+			// ])
+			// .arg(&built);
+			// if let Err(e) = cmd.sudo().run_nix().await {
+			// 	error!("failed to switch system profile generation: {e}");
+			// 	failed = true;
+			// }
+			// It would also be possible to update profile atomically during copy:
+			// https://github.com/NixOS/nix/pull/11657
+			let mut cmd = host.nix_cmd().await?;
+			cmd.arg("build");
+			cmd.comparg("--profile", "/nix/var/nix/profiles/system");
+			cmd.arg(&built);
+			if let Err(e) = cmd.sudo().run_nix().await {
+				error!("failed to switch system profile generation: {e}");
+				failed = true;
+			}
+		}
+
+		// FIXME: Connection might be disconnected after activation run
+
+		if action.should_activate() && !failed {
+			let _span = info_span!("activating").entered();
+			info!("executing activation script");
+			let specialised = if let Some(specialisation) = specialisation {
+				let mut specialised = built.join("specialisation");
+				specialised.push(specialisation);
+				specialised
+			} else {
+				built.clone()
+			};
+			let switch_script = specialised.join("bin/switch-to-configuration");
+			let mut cmd = host.cmd(switch_script).in_current_span().await?;
+			if deploy_kind == DeployKind::NixosLustrate {
+				cmd.env("NIXOS_INSTALL_BOOTLOADER", "1");
+			}
+			cmd.env("FLEET_ONLINE_ACTIVATION", "1")
+				.arg(action.name().expect("upload.should_activate == false"));
+			if let Err(e) = cmd.sudo().run().in_current_span().await {
+				error!("failed to activate: {e}");
+				failed = true;
+			}
+		}
+	}
+	if action.should_create_rollback_marker() {
+		if !disable_rollback {
+			if failed {
+				if action.should_schedule_rollback_run() {
+					info!("executing rollback");
+					if let Err(e) = host
+						.systemctl_start("rollback-watchdog.service")
+						.instrument(info_span!("rollback"))
+						.await
+					{
+						error!("failed to trigger rollback: {e}")
+					}
+				}
+			} else {
+				info!("trying to mark upgrade as successful");
+				if let Err(e) = host
+					.rm_file("/etc/fleet_rollback_marker", true)
+					.in_current_span()
+					.await
+				{
+					error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
+				}
+			}
+			info!("disarming watchdog, just in case");
+			if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
+				// It is ok, if there was no reboot - then timer might not be running.
+			}
+			if action.should_schedule_rollback_run() {
+				if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
+					error!("failed to disarm rollback run: {e}");
+				}
+			}
+		} else if let Err(_e) = host
+			.rm_file("/etc/fleet_rollback_marker", true)
+			.in_current_span()
+			.await
+		{
+			// Marker might not exist, yet better try to remove it.
+		}
+	}
+	Ok(())
+}
+
+pub async fn upload_task(
+	config: &Config,
+	host: &ConfigHost,
+	location: GenerationStorage,
+	generation: PathBuf,
+) -> Result<PathBuf> {
+	let local_host = config.local_host();
+	if matches!(location, GenerationStorage::Pusher) {
+		bail!("pusher is not enabled in this version of fleet");
+	}
+	if !host.local {
+		info!("uploading system closure");
+		{
+			// TODO: Move to remote_derivation method.
+			// Alternatively, nix store make-content-addressed can be used,
+			// at least for the first deployment, to provide trusted store key.
+			//
+			// It is much slower, yet doesn't require root on the deployer machine.
+			let Ok(mut sign) = local_host.cmd("nix").await else {
+				bail!("failed to setup local");
+			};
+			// Private key for host machine is registered in nix-sign.nix
+			sign.arg("store")
+				.arg("sign")
+				.comparg("--key-file", "/etc/nix/private-key")
+				.arg("-r")
+				.arg(&generation);
+			if let Err(e) = sign.sudo().run_nix().await {
+				warn!("failed to sign store paths: {e}");
+			};
+		}
+		let mut tries = 0;
+		loop {
+			match host.remote_derivation(&generation).await {
+				Ok(remote) => {
+					assert!(remote == generation, "CA derivations aren't implemented");
+					return Ok(remote);
+				}
+				Err(e) if tries < 3 => {
+					tries += 1;
+					warn!("copy failure ({}/3): {}", tries, e);
+					sleep(Duration::from_millis(5000)).await;
+				}
+				Err(e) => {
+					bail!("upload failed: {e}");
+				}
+			}
+		}
+	}
+	Ok(generation)
+}
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -7,8 +7,8 @@
 use chrono::{DateTime, Utc};
 use fleet_shared::SecretData;
 use rand::{
-	distributions::{Alphanumeric, DistString},
-	thread_rng,
+	distr::{Alphanumeric, SampleString as _},
+	rng,
 };
 use serde::{de::Error, Deserialize, Serialize};
 use serde_json::Value;
@@ -47,7 +47,7 @@
 }
 
 fn generate_gc_prefix() -> String {
-	let id = Alphanumeric.sample_string(&mut thread_rng(), 8);
+	let id = Alphanumeric.sample_string(&mut rng(), 8);
 	format!("fleet-gc-{id}")
 }
 
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -15,7 +15,10 @@
 use nix_eval::{nix_go, nix_go_json, util::assert_warn, NixSession, Value};
 use openssh::SessionBuilder;
 use serde::de::DeserializeOwned;
+use tabled::Tabled;
 use tempfile::NamedTempFile;
+use time::{format_description, UtcDateTime};
+use tracing::warn;
 
 use crate::{
 	command::MyCommand,
@@ -104,8 +107,106 @@
 	pub local: bool,
 	pub session: OnceLock<Arc<openssh::Session>>,
 }
+
+#[derive(Debug, Clone, Copy)]
+pub enum GenerationStorage {
+	Deployer,
+	Machine,
+	Pusher,
+}
+impl GenerationStorage {
+	fn prefix(&self) -> &'static str {
+		match self {
+			GenerationStorage::Deployer => "deployer.",
+			GenerationStorage::Machine => "",
+			GenerationStorage::Pusher => "pusher.",
+		}
+	}
+}
+
+#[derive(Tabled, Debug)]
+pub struct Generation {
+	#[tabled(rename = "ID", format("{}", self.rollback_id()))]
+	pub id: u32,
+	#[tabled(rename = "Current")]
+	pub current: bool,
+	#[tabled(rename = "Created at")]
+	pub datetime: UtcDateTime,
+	#[tabled(format = "{:?}")]
+	pub store_path: PathBuf,
+	#[tabled(skip)]
+	pub location: GenerationStorage,
+}
+impl Generation {
+	pub fn rollback_id(&self) -> String {
+		format!("{}{}", self.location.prefix(), self.id)
+	}
+}
+
+fn parse_generation_line(g: &str) -> Option<Generation> {
+	let mut parts = g.split_whitespace();
+	let id = parts.next()?;
+	let id: u32 = id.parse().ok()?;
+	let date = parts.next()?;
+	let time = parts.next()?;
+	let current = if let Some(current) = parts.next() {
+		if current == "(current)" {
+			Some(true)
+		} else {
+			None
+		}
+	} else {
+		Some(false)
+	};
+	let current = current?;
+	if parts.next().is_some() {
+		warn!("unexpected text after generation: {g}");
+	}
+
+	let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")
+		.expect("valid format");
+	let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;
+
+	Some(Generation {
+		id,
+		current,
+		datetime,
+		store_path: PathBuf::new(),
+		location: GenerationStorage::Machine,
+	})
+}
 // TODO: Move command helpers away with connectivity refactor
 impl ConfigHost {
+	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {
+		let mut cmd = self.cmd("nix-env").await?;
+		cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))
+			.arg("--list-generations")
+			.env("TZ", "UTC");
+		// Sudo is required because --list-generations tries to acquire profile lock
+		let data = cmd.sudo().run_string().await?;
+		let mut generations = data
+			.split('\n')
+			.map(|e| e.trim())
+			.filter(|&l| !l.is_empty())
+			.filter_map(|g| {
+				let gen = parse_generation_line(g);
+				if gen.is_none() {
+					warn!("bad generation: {g}");
+				};
+				gen
+			})
+			.collect::<Vec<_>>();
+		for ele in generations.iter_mut() {
+			let mut cmd = self.cmd("readlink").await?;
+			cmd.arg("--")
+				.arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));
+			let path = cmd.run_string().await?;
+			ele.store_path = PathBuf::from(path.trim_end_matches("\n"));
+		}
+
+		Ok(generations)
+	}
+
 	pub fn set_deploy_kind(&self, kind: DeployKind) {
 		self.deploy_kind
 			.set(kind)
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -3,3 +3,4 @@
 pub mod host;
 mod keys;
 pub mod opts;
+pub mod deploy;
\ No newline at end of file
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -7,7 +7,6 @@
 };
 
 use anyhow::{bail, Context, Result};
-use clap::Parser;
 use nix_eval::{nix_go, util::assert_warn, NixSessionPool, Value};
 use nom::{
 	bytes::complete::take_while1,
@@ -15,6 +14,7 @@
 	combinator::{map, opt},
 	multi::separated_list1,
 	sequence::{preceded, separated_pair},
+	Parser,
 };
 
 use crate::{
@@ -38,11 +38,13 @@
 		err.to_string()
 	}
 
-	let (input, is_tag) = map(opt(char('@')), |c| c.is_some())(input).map_err(err_to_string)?;
+	let (input, is_tag) = map(opt(char('@')), |c| c.is_some())
+		.parse_complete(input)
+		.map_err(err_to_string)?;
 	let (input, name) = map(
 		take_while1(|v| v != ',' && v != '?' && v != '@'),
 		str::to_owned,
-	)(input)
+	).parse_complete(input)
 	.map_err(err_to_string)?;
 
 	let kw_item = separated_pair(
@@ -55,7 +57,7 @@
 	});
 	let mut opt_kw = map(opt(preceded(char('?'), kw)), Option::unwrap_or_default);
 
-	let (input, attrs) = opt_kw(input).map_err(err_to_string)?;
+	let (input, attrs) = opt_kw.parse_complete(input).map_err(err_to_string)?;
 
 	if !input.is_empty() {
 		return Err(format!("unexpected trailing input: {input:?}"));
@@ -68,7 +70,7 @@
 }
 
 // TODO: Rename to HostSelector
-#[derive(Parser, Clone)]
+#[derive(clap::Parser, Clone)]
 pub struct FleetOpts {
 	/// All hosts except those would be skipped
 	#[clap(long, number_of_values = 1, value_parser = host_item_parser)]
modifiedcrates/fleet-shared/Cargo.tomldiffbeforeafterboth
--- a/crates/fleet-shared/Cargo.toml
+++ b/crates/fleet-shared/Cargo.toml
@@ -6,6 +6,6 @@
 
 [dependencies]
 base64 = "0.22.1"
-serde = "1.0.202"
+serde = "1.0.219"
 unicode_categories = "0.1.1"
-z85 = "3.0.5"
+z85 = "3.0.6"
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -16,11 +16,11 @@
 tokio-util.workspace = true
 tracing.workspace = true
 
-futures = "0.3.30"
-itertools = "0.13.0"
+futures = "0.3.31"
+itertools = "0.14.0"
 r2d2 = "0.8.10"
-regex = "1.10.6"
-unindent = "0.2.3"
+regex = "1.11.1"
+unindent = "0.2.4"
 
 # [build-dependencies]
 # bindgen = "0.69.4"
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -9,8 +9,8 @@
 
 alejandra = { git = "https://github.com/kamadorueda/alejandra" }
 linked-hash-map = "0.5.6"
-peg = "0.8.2"
-ron = "0.8.1"
-serde = "1.0.196"
+peg = "0.8.5"
+ron = "0.10.1"
+serde = "1.0.219"
 serde-transcode = "1.1.1"
-serde_json = "1.0.113"
+serde_json = "1.0.140"