diff options
Diffstat (limited to 'src/cli.rs')
| -rw-r--r-- | src/cli.rs | 303 |
1 files changed, 227 insertions, 76 deletions
@@ -1,57 +1,114 @@ -use clap::{Parser, Subcommand}; +use argh::FromArgs; use std::path::PathBuf; -/// Witryna - minimalist Git-based static site deployment orchestrator -#[derive(Debug, Parser)] -#[command( - name = "witryna", - version, - author, - about = "Minimalist Git-based static site deployment orchestrator", - long_about = "Minimalist Git-based static site deployment orchestrator.\n\n\ - Witryna listens for webhook HTTP requests, pulls the corresponding Git \ - repository (with automatic Git LFS fetch and submodule initialization), \ - runs a user-defined build command inside an ephemeral container and \ - publishes the resulting assets via atomic symlink switching.\n\n\ - A health-check endpoint is available at GET /health (returns 200 OK).\n\n\ - Witryna does not serve files, terminate TLS, or manage DNS. \ - It is designed to sit behind a reverse proxy (Nginx, Caddy, etc.).", - subcommand_required = true, - arg_required_else_help = true -)] +/// Minimalist Git-based static site deployment orchestrator +#[derive(Debug, FromArgs)] pub struct Cli { - /// Path to the configuration file. - /// If not specified, searches: ./witryna.toml, $XDG_CONFIG_HOME/witryna/witryna.toml, /etc/witryna/witryna.toml - #[arg(long, global = true, value_name = "FILE")] - pub config: Option<PathBuf>, - - #[command(subcommand)] + #[argh(subcommand)] pub command: Command, } -#[derive(Debug, Subcommand)] +#[derive(Debug, FromArgs)] +#[argh(subcommand)] pub enum Command { - /// Start the deployment server (foreground) - Serve, - /// Validate configuration file and print summary - Validate, - /// Trigger a one-off build for a site (synchronous, no server) - Run { - /// Site name (as defined in witryna.toml) - site: String, - /// Stream full build output to stderr in real-time - #[arg(long, short)] - verbose: bool, - }, - /// Show deployment status for configured sites - Status { - /// Show last 10 deployments for a single site - #[arg(long, short)] - site: Option<String>, - /// Output in JSON format - #[arg(long)] - json: bool, - }, + Serve(ServeCmd), + Validate(ValidateCmd), + Run(RunCmd), + Status(StatusCmd), + Switch(SwitchCmd), + Cleanup(CleanupCmd), +} + +impl Command { + #[must_use] + pub fn config(&self) -> Option<&std::path::Path> { + match self { + Self::Serve(c) => c.config.as_deref(), + Self::Validate(c) => c.config.as_deref(), + Self::Run(c) => c.config.as_deref(), + Self::Status(c) => c.config.as_deref(), + Self::Switch(c) => c.config.as_deref(), + Self::Cleanup(c) => c.config.as_deref(), + } + } +} + +/// Start the deployment server (foreground) +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "serve")] +pub struct ServeCmd { + /// path to configuration file + #[argh(option)] + pub config: Option<PathBuf>, +} + +/// Validate configuration file and print summary +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "validate")] +pub struct ValidateCmd { + /// path to configuration file + #[argh(option)] + pub config: Option<PathBuf>, +} + +/// Trigger a one-off build for a site (synchronous, no server) +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "run")] +pub struct RunCmd { + /// path to configuration file + #[argh(option)] + pub config: Option<PathBuf>, + /// site name (as defined in witryna.toml) + #[argh(positional)] + pub site: String, + /// stream full build output to stderr in real-time + #[argh(switch, short = 'v')] + pub verbose: bool, +} + +/// Show deployment status for configured sites +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "status")] +pub struct StatusCmd { + /// path to configuration file + #[argh(option)] + pub config: Option<PathBuf>, + /// site name (if omitted, shows all sites) + #[argh(positional)] + pub site: Option<String>, + /// output in JSON format + #[argh(switch)] + pub json: bool, +} + +/// Switch the active build symlink (rollback) +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "switch")] +pub struct SwitchCmd { + /// path to configuration file + #[argh(option)] + pub config: Option<PathBuf>, + /// site name (as defined in witryna.toml) + #[argh(positional)] + pub site: String, + /// build timestamp to switch to (from `witryna status <site>`) + #[argh(positional)] + pub build: String, +} + +/// Remove old builds and logs +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "cleanup")] +pub struct CleanupCmd { + /// path to configuration file + #[argh(option)] + pub config: Option<PathBuf>, + /// site name (if omitted, cleans all sites) + #[argh(positional)] + pub site: Option<String>, + /// number of builds to keep per site (overrides `max_builds_to_keep`) + #[argh(option)] + pub keep: Option<u32>, } #[cfg(test)] @@ -61,74 +118,168 @@ mod tests { #[test] fn run_parses_site_name() { - let cli = Cli::try_parse_from(["witryna", "run", "my-site"]).unwrap(); + let cli = Cli::from_args(&["witryna"], &["run", "my-site"]).unwrap(); match cli.command { - Command::Run { site, verbose } => { - assert_eq!(site, "my-site"); - assert!(!verbose); + Command::Run(cmd) => { + assert_eq!(cmd.site, "my-site"); + assert!(!cmd.verbose); } - _ => panic!("expected Run command"), + _ => unreachable!("expected Run command"), } } #[test] fn run_parses_verbose_flag() { - let cli = Cli::try_parse_from(["witryna", "run", "my-site", "--verbose"]).unwrap(); + let cli = Cli::from_args(&["witryna"], &["run", "my-site", "--verbose"]).unwrap(); match cli.command { - Command::Run { site, verbose } => { - assert_eq!(site, "my-site"); - assert!(verbose); + Command::Run(cmd) => { + assert_eq!(cmd.site, "my-site"); + assert!(cmd.verbose); } - _ => panic!("expected Run command"), + _ => unreachable!("expected Run command"), } } #[test] fn status_parses_without_flags() { - let cli = Cli::try_parse_from(["witryna", "status"]).unwrap(); + let cli = Cli::from_args(&["witryna"], &["status"]).unwrap(); match cli.command { - Command::Status { site, json } => { - assert!(site.is_none()); - assert!(!json); + Command::Status(cmd) => { + assert!(cmd.site.is_none()); + assert!(!cmd.json); } - _ => panic!("expected Status command"), + _ => unreachable!("expected Status command"), } } #[test] fn status_parses_site_filter() { - let cli = Cli::try_parse_from(["witryna", "status", "--site", "my-site"]).unwrap(); + let cli = Cli::from_args(&["witryna"], &["status", "my-site"]).unwrap(); match cli.command { - Command::Status { site, json } => { - assert_eq!(site.as_deref(), Some("my-site")); - assert!(!json); + Command::Status(cmd) => { + assert_eq!(cmd.site.as_deref(), Some("my-site")); + assert!(!cmd.json); } - _ => panic!("expected Status command"), + _ => unreachable!("expected Status command"), } } #[test] fn status_parses_json_flag() { - let cli = Cli::try_parse_from(["witryna", "status", "--json"]).unwrap(); + let cli = Cli::from_args(&["witryna"], &["status", "--json"]).unwrap(); + match cli.command { + Command::Status(cmd) => { + assert!(cmd.site.is_none()); + assert!(cmd.json); + } + _ => unreachable!("expected Status command"), + } + } + + #[test] + fn cleanup_parses_without_args() { + let cli = Cli::from_args(&["witryna"], &["cleanup"]).unwrap(); + match cli.command { + Command::Cleanup(cmd) => { + assert!(cmd.site.is_none()); + assert!(cmd.keep.is_none()); + } + _ => unreachable!("expected Cleanup command"), + } + } + + #[test] + fn cleanup_parses_site_name() { + let cli = Cli::from_args(&["witryna"], &["cleanup", "my-site"]).unwrap(); + match cli.command { + Command::Cleanup(cmd) => { + assert_eq!(cmd.site.as_deref(), Some("my-site")); + assert!(cmd.keep.is_none()); + } + _ => unreachable!("expected Cleanup command"), + } + } + + #[test] + fn cleanup_parses_keep_flag() { + let cli = Cli::from_args(&["witryna"], &["cleanup", "--keep", "3"]).unwrap(); + match cli.command { + Command::Cleanup(cmd) => { + assert!(cmd.site.is_none()); + assert_eq!(cmd.keep, Some(3)); + } + _ => unreachable!("expected Cleanup command"), + } + } + + #[test] + fn cleanup_parses_site_and_keep() { + let cli = Cli::from_args(&["witryna"], &["cleanup", "my-site", "--keep", "3"]).unwrap(); + match cli.command { + Command::Cleanup(cmd) => { + assert_eq!(cmd.site.as_deref(), Some("my-site")); + assert_eq!(cmd.keep, Some(3)); + } + _ => unreachable!("expected Cleanup command"), + } + } + + #[test] + fn switch_parses_site_and_build() { + let cli = Cli::from_args( + &["witryna"], + &["switch", "my-site", "20260126-143000-123456"], + ) + .unwrap(); + match cli.command { + Command::Switch(cmd) => { + assert_eq!(cmd.site, "my-site"); + assert_eq!(cmd.build, "20260126-143000-123456"); + assert!(cmd.config.is_none()); + } + _ => unreachable!("expected Switch command"), + } + } + + #[test] + fn switch_parses_config_flag() { + let cli = Cli::from_args( + &["witryna"], + &[ + "switch", + "--config", + "/etc/witryna.toml", + "my-site", + "20260126-143000-123456", + ], + ) + .unwrap(); match cli.command { - Command::Status { site, json } => { - assert!(site.is_none()); - assert!(json); + Command::Switch(cmd) => { + assert_eq!(cmd.site, "my-site"); + assert_eq!(cmd.build, "20260126-143000-123456"); + assert_eq!( + cmd.config, + Some(std::path::PathBuf::from("/etc/witryna.toml")) + ); } - _ => panic!("expected Status command"), + _ => unreachable!("expected Switch command"), } } #[test] fn config_flag_is_optional() { - let cli = Cli::try_parse_from(["witryna", "status"]).unwrap(); - assert!(cli.config.is_none()); + let cli = Cli::from_args(&["witryna"], &["status"]).unwrap(); + assert!(cli.command.config().is_none()); } #[test] fn config_flag_explicit_path() { let cli = - Cli::try_parse_from(["witryna", "--config", "/etc/witryna.toml", "status"]).unwrap(); - assert_eq!(cli.config, Some(PathBuf::from("/etc/witryna.toml"))); + Cli::from_args(&["witryna"], &["status", "--config", "/etc/witryna.toml"]).unwrap(); + assert_eq!( + cli.command.config(), + Some(std::path::Path::new("/etc/witryna.toml")) + ); } } |
