feat: add damp run type (#1217)

Co-authored-by: Stuart Reilly <sreilly@scottlogic.com>
This commit is contained in:
Stuart Reilly
2025-11-02 14:06:35 +00:00
committed by GitHub
parent 8fc25d7fd4
commit 99892359c7
12 changed files with 164 additions and 109 deletions

View File

@@ -18,8 +18,10 @@ assignees: ''
option to skip this step?
## I want to suggest some general feature
Topgrade should...
## More information
<!-- Assuming that someone else implements the feature,
please state if you know how to test it from a side branch of Topgrade. -->

View File

@@ -1,6 +1,5 @@
## What does this PR do
## Standards checklist
- [ ] The PR title is descriptive

View File

@@ -15,8 +15,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5.0.0
- run: |
- uses: actions/checkout@v5.0.0
- run: |
CONFIG_PATH=~/.config/topgrade.toml;
if [ -f "$CONFIG_PATH" ]; then rm $CONFIG_PATH; fi
cargo build;

View File

@@ -1,3 +1,3 @@
1. The `jet_brains_toolbox` step was renamed to `jetbrains_toolbox`. If you're
using the old name in your configuration file in the `disable` or `only`
fields, simply change it to `jetbrains_toolbox`.
fields, simply change it to `jetbrains_toolbox`.

View File

@@ -16,6 +16,22 @@ _version: 2
zh_CN: "正在模拟 %{program_name} %{arguments} 的运行过程"
zh_TW: "正在模擬 %{program_name} %{arguments} 的執行過程"
de: "Testlauf: %{program_name} %{arguments}"
"Executing: {program_name} {arguments}":
en: "Executing: %{program_name} %{arguments}"
lt: "Vykdymas: %{program_name} %{arguments}"
es: "Ejecutando: %{program_name} %{arguments}"
fr: "Exécution: %{program_name} %{arguments}"
zh_CN: "执行: %{program_name} %{arguments}"
zh_TW: "执行: %{program_name} %{arguments}"
de: "Ausführung: %{program_name} %{arguments}"
"with env: {env}":
en: "with env: %{env}"
lt: "su env: %{env}"
es: "con env: %{env}"
fr: "avec env: %{env}"
zh_CN: "与env %{env}"
zh_TW: "与env %{env}"
de: "mit env: %{env}"
"in {directory}":
en: "in %{directory}"
lt: "kataloge %{directory}"
@@ -1298,30 +1314,14 @@ _version: 2
zh_CN: "Windows 更新"
zh_TW: "Windows 更新"
de: "Windows-Update"
"Would check if OpenBSD is -current":
en: "Would check if OpenBSD is -current"
lt: "Patikrintų, ar OpenBSD yra -current"
es: "Comprobaría si OpenBSD está en -current"
fr: "Vérifierait si OpenBSD est à -curent"
zh_CN: "检查 OpenBSD 是否 -current"
zh_TW: "會檢查 OpenBSD 是否 -current"
de: "Würde überprüfen, ob OpenBSD -current ist"
"Would upgrade the OpenBSD system":
en: "Would upgrade the OpenBSD system"
lt: "Atnaujintų OpenBSD sistemą"
es: "Actualizaría el sistema OpenBSD"
fr: "Mettrait à jour le système OpenBSD"
zh_CN: "将升级 OpenBSD 系统"
zh_TW: "會升級 OpenBSD 系統"
de: "Würde das OpenBSD-System aktualisieren"
"Would upgrade OpenBSD packages":
en: "Would upgrade OpenBSD packages"
lt: "Atnaujintų OpenBSD paketus"
es: "Actualizaría los paquetes de OpenBSD"
fr: "Mettrait à jour les paquets OpenBSD"
zh_CN: "将升级 OpenBSD 软件包"
zh_TW: "會升級 OpenBSD 套件"
de: "Würde OpenBSD-Pakete aktualisieren"
"Checking if /etc/motd contains -current or -beta":
en: "Checking if /etc/motd contains -current or -beta"
lt: "Tikrinimas, jei /etc/motd yra -current arba -beta"
es: "Comprobación de si /etc/motd contiene -current o -beta"
fr: "Vérification si /etc/motd contient -current ou -beta"
zh_CN: "检查 /etc/motd 是否包含 -current 或 -beta"
zh_TW: "检查 /etc/motd 是否包含 -current 或 -beta"
de: "Überprüfen, ob /etc/motd -current oder -beta enthält"
"Microsoft Store":
en: "Microsoft Store"
lt: "Microsoft parduotuvė"

View File

@@ -18,14 +18,15 @@ use regex_split::RegexSplit;
use rust_i18n::t;
use serde::Deserialize;
use strum::IntoEnumIterator;
use tracing::{debug, error};
use which_crate::which;
use super::utils::editor;
use crate::command::CommandExt;
use crate::execution_context::RunType;
use crate::step::Step;
use crate::sudo::SudoKind;
use crate::utils::string_prepend_str;
use tracing::{debug, error};
// TODO: Add i18n to this. Tracking issue: https://github.com/topgrade-rs/topgrade/issues/859
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
@@ -708,9 +709,15 @@ pub struct CommandLineArgs {
cleanup: bool,
/// Print what would be done
///
/// Alias for --run-type dry
#[arg(short = 'n', long = "dry-run")]
dry_run: bool,
/// Pick between just running commands, running and logging commands, and just logging commands
#[arg(short = 'r', long = "run-type", value_enum, default_value_t)]
run_type: RunType,
/// Do not ask to retry failed steps
#[arg(long = "no-retry")]
no_retry: bool,
@@ -1001,9 +1008,13 @@ impl Config {
.unwrap_or(false)
}
/// Tell whether we are dry-running.
pub fn dry_run(&self) -> bool {
self.opt.dry_run
/// Get the [RunType] for the current execution
pub fn run_type(&self) -> RunType {
if self.opt.dry_run {
RunType::Dry
} else {
self.opt.run_type
}
}
/// Tell whether we should not attempt to retry anything.

View File

@@ -1,11 +1,15 @@
#![allow(dead_code)]
use color_eyre::eyre::Result;
use rust_i18n::t;
use std::env::var;
use std::ffi::OsStr;
use std::process::Command;
use std::sync::{LazyLock, Mutex};
use clap::ValueEnum;
use color_eyre::eyre::Result;
use rust_i18n::t;
use serde::Deserialize;
use strum::EnumString;
use crate::config::Config;
use crate::error::MissingSudo;
use crate::executor::{DryCommand, Executor};
@@ -16,30 +20,26 @@ use crate::sudo::Sudo;
use crate::utils::require_option;
/// An enum telling whether Topgrade should perform dry runs or actually perform the steps.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Deserialize, Default, EnumString, ValueEnum)]
pub enum RunType {
/// Executing commands will just print the command with its argument.
Dry,
/// Executing commands will perform actual execution.
#[default]
Wet,
/// Executing commands will print the command and perform actual execution.
Damp,
}
impl RunType {
/// Create a new instance from a boolean telling whether to dry run.
pub fn new(dry_run: bool) -> Self {
if dry_run {
RunType::Dry
} else {
RunType::Wet
}
}
/// Tells whether we're performing a dry run.
pub fn dry(self) -> bool {
match self {
RunType::Dry => true,
RunType::Wet => false,
RunType::Damp => false,
}
}
}
@@ -84,6 +84,7 @@ impl<'a> ExecutionContext<'a> {
match self.run_type {
RunType::Dry => Executor::Dry(DryCommand::new(program)),
RunType::Wet => Executor::Wet(Command::new(program)),
RunType::Damp => Executor::Damp(Command::new(program)),
}
}

View File

@@ -1,11 +1,13 @@
//! Utilities for command execution
use std::ffi::{OsStr, OsString};
use std::fmt::Debug;
use std::iter;
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Output};
use color_eyre::eyre::Result;
use rust_i18n::t;
use tracing::debug;
use tracing::{debug, enabled, Level};
use crate::command::CommandExt;
use crate::error::DryRun;
@@ -15,6 +17,7 @@ use crate::error::DryRun;
/// If the enum is set to `Dry`, execution will just print the command with its arguments.
pub enum Executor {
Wet(Command),
Damp(Command),
Dry(DryCommand),
}
@@ -24,7 +27,7 @@ impl Executor {
/// Will give weird results for non-UTF-8 programs; see `to_string_lossy()`.
pub fn get_program(&self) -> String {
match self {
Executor::Wet(c) => c.get_program().to_string_lossy().into_owned(),
Executor::Wet(c) | Executor::Damp(c) => c.get_program().to_string_lossy().into_owned(),
Executor::Dry(c) => c.program.to_string_lossy().into_owned(),
}
}
@@ -32,7 +35,7 @@ impl Executor {
/// See `std::process::Command::arg`
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Executor {
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.arg(arg);
}
Executor::Dry(c) => {
@@ -50,7 +53,7 @@ impl Executor {
S: AsRef<OsStr>,
{
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.args(args);
}
Executor::Dry(c) => {
@@ -65,7 +68,7 @@ impl Executor {
/// See `std::process::Command::current_dir`
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Executor {
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.current_dir(dir);
}
Executor::Dry(c) => c.directory = Some(dir.as_ref().into()),
@@ -81,7 +84,7 @@ impl Executor {
K: AsRef<OsStr>,
{
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.env_remove(key);
}
Executor::Dry(_) => (),
@@ -98,7 +101,7 @@ impl Executor {
V: AsRef<OsStr>,
{
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
c.env(key, val);
}
Executor::Dry(_) => (),
@@ -109,18 +112,16 @@ impl Executor {
/// See `std::process::Command::spawn`
pub fn spawn(&mut self) -> Result<ExecutorChild> {
self.log_command();
let result = match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
debug!("Running {:?}", c);
// We should use `spawn()` here rather than `spawn_checked()` since
// their semantics and behaviors are different.
#[allow(clippy::disallowed_methods)]
c.spawn().map(ExecutorChild::Wet)?
}
Executor::Dry(c) => {
c.dry_run();
ExecutorChild::Dry
}
Executor::Dry(_) => ExecutorChild::Dry,
};
Ok(result)
@@ -128,17 +129,15 @@ impl Executor {
/// See `std::process::Command::output`
pub fn output(&mut self) -> Result<ExecutorOutput> {
self.log_command();
match self {
Executor::Wet(c) => {
Executor::Wet(c) | Executor::Damp(c) => {
// We should use `output()` here rather than `output_checked()` since
// their semantics and behaviors are different.
#[allow(clippy::disallowed_methods)]
Ok(ExecutorOutput::Wet(c.output()?))
}
Executor::Dry(c) => {
c.dry_run();
Ok(ExecutorOutput::Dry)
}
Executor::Dry(_) => Ok(ExecutorOutput::Dry),
}
}
@@ -146,18 +145,38 @@ impl Executor {
/// that can indicate success of a script
#[allow(dead_code)]
pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> {
self.log_command();
match self {
Executor::Wet(c) => c.status_checked_with(|status| {
Executor::Wet(c) | Executor::Damp(c) => c.status_checked_with(|status| {
if status.success() || status.code().as_ref().is_some_and(|c| codes.contains(c)) {
Ok(())
} else {
Err(())
}
}),
Executor::Dry(c) => {
c.dry_run();
Ok(())
Executor::Dry(_) => Ok(()),
}
}
fn log_command(&self) {
match self {
Executor::Wet(_) => (),
Executor::Damp(c) => {
log_command(
"Executing: {program_name} {arguments}",
c.get_program(),
c.get_args(),
c.get_envs(),
c.get_current_dir(),
);
}
Executor::Dry(c) => log_command(
"Dry running: {program_name} {arguments}",
&c.program,
&c.args,
iter::empty(),
c.directory.as_ref(),
),
}
}
}
@@ -182,30 +201,11 @@ impl DryCommand {
directory: None,
}
}
fn dry_run(&self) {
print!(
"{}",
t!(
"Dry running: {program_name} {arguments}",
program_name = self.program.to_string_lossy(),
arguments = shell_words::join(
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
)
)
);
match &self.directory {
Some(dir) => println!(" {}", t!("in {directory}", directory = dir.to_string_lossy())),
None => println!(),
};
}
}
/// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command.
pub enum ExecutorChild {
// Both RunType::Wet and RunType::Damp use this variant
#[allow(unused)] // this type has not been used
Wet(Child),
Dry,
@@ -218,22 +218,18 @@ impl CommandExt for Executor {
// variant for wet/dry runs.
fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> Result<Output> {
self.log_command();
match self {
Executor::Wet(c) => c.output_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Err(DryRun().into())
}
Executor::Wet(c) | Executor::Damp(c) => c.output_checked_with(succeeded),
Executor::Dry(_) => Err(DryRun().into()),
}
}
fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> Result<()> {
self.log_command();
match self {
Executor::Wet(c) => c.status_checked_with(succeeded),
Executor::Dry(c) => {
c.dry_run();
Ok(())
}
Executor::Wet(c) | Executor::Damp(c) => c.status_checked_with(succeeded),
Executor::Dry(_) => Ok(()),
}
}
@@ -241,3 +237,42 @@ impl CommandExt for Executor {
self.spawn()
}
}
fn log_command<
'a,
I: ExactSizeIterator<Item = (&'a (impl Debug + 'a + ?Sized), Option<&'a (impl Debug + 'a + ?Sized)>)>,
>(
prefix: &str,
exec: &OsStr,
args: impl IntoIterator<Item = &'a (impl AsRef<OsStr> + ?Sized + 'a)>,
env: impl IntoIterator<Item = (&'a OsStr, Option<&'a OsStr>), IntoIter = I>,
dir: Option<&'a (impl AsRef<Path> + ?Sized)>,
) {
println!(
"{}",
t!(
prefix,
program_name = exec.to_string_lossy(),
arguments = shell_words::join(args.into_iter().map(|s| s.as_ref().to_string_lossy()))
)
);
let env_iter = env.into_iter();
if env_iter.len() != 0 && enabled!(Level::DEBUG) {
println!(
" {}",
t!(
"with env: {env}",
env = env_iter
.filter(|(_, val)| val.is_some())
.map(|(key, val)| format!("{:?}={:?}", key, val.unwrap()))
.collect::<Vec<_>>()
.join(" ")
)
)
}
if let Some(d) = dir {
println!(" {}", t!("in {directory}", directory = d.as_ref().display()));
}
}

View File

@@ -124,7 +124,7 @@ fn run() -> Result<()> {
debug!("Version: {}", crate_version!());
debug!("OS: {}", env!("TARGET"));
debug!("{:?}", env::args());
debug!("Binary path: {:?}", std::env::current_exe());
debug!("Binary path: {:?}", env::current_exe());
debug!("self-update Feature Enabled: {:?}", cfg!(feature = "self-update"));
debug!("Configuration: {:?}", config);
@@ -163,7 +163,7 @@ fn run() -> Result<()> {
#[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect();
let run_type = execution_context::RunType::new(config.dry_run());
let run_type = config.run_type();
let ctx = execution_context::ExecutionContext::new(
run_type,
sudo,

View File

@@ -38,7 +38,7 @@ pub fn run_mas(ctx: &ExecutionContext) -> Result<()> {
pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> {
print_separator(t!("macOS system update"));
let should_ask = !(ctx.config().yes(Step::System) || ctx.config().dry_run());
let should_ask = !(ctx.config().yes(Step::System) || ctx.run_type().dry());
if should_ask {
println!("{}", t!("Finding available software"));
if system_update_available()? {
@@ -95,7 +95,7 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> {
let xcodes = require("xcodes")?;
print_separator("Xcodes");
let should_ask = !(ctx.config().yes(Step::Xcodes) || ctx.config().dry_run());
let should_ask = !(ctx.config().yes(Step::Xcodes) || ctx.run_type().dry());
let releases = ctx.execute(&xcodes).args(["update"]).output_checked_utf8()?.stdout;

View File

@@ -1,5 +1,6 @@
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::executor::RunType;
use crate::terminal::print_separator;
use color_eyre::eyre::Result;
use rust_i18n::t;
@@ -8,12 +9,13 @@ use std::fs;
fn is_openbsd_current(ctx: &ExecutionContext) -> Result<bool> {
let motd_content = fs::read_to_string("/etc/motd")?;
let is_current = ["-current", "-beta"].iter().any(|&s| motd_content.contains(s));
if ctx.config().dry_run() {
println!("{}", t!("Would check if OpenBSD is -current"));
Ok(is_current)
} else {
Ok(is_current)
match ctx.config.run_type() {
RunType::Dry | RunType::Damp => {
println!("{}", t!("Checking if /etc/motd contains -current or -beta"));
}
RunType::Wet => {}
}
Ok(is_current)
}
pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> {
@@ -42,11 +44,6 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
let is_current = is_openbsd_current(ctx)?;
if ctx.config().dry_run() {
println!("{}", t!("Would upgrade OpenBSD packages"));
return Ok(());
}
if ctx.config().cleanup() {
sudo.execute(ctx, "/usr/sbin/pkg_delete")?.arg("-ac").status_checked()?;
}

10
translate.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
## Translate the given string into $langs using translate-shell, outputting to the yaml structure expected for locales/app.yml
langs="en lt es fr zh_CN zh_TW de"
printf "\"%s\":\n" "$@"
for lang in $langs; do
result=$(trans -brief -no-auto -s en -t "${lang/_/-/}" "$@")
printf " %s: \"%s\"\n" "$lang" "$result"
done