use clap::{Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(ValueEnum, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UpgradeStrategy {
Latest,
Major,
Minor,
Commit,
}
#[derive(ValueEnum, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
#[default]
Text,
Json,
Markdown,
}
#[derive(Parser, Clone)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(
short,
long,
global = true,
help = "Workflow files or directories to process"
)]
pub workflows: Vec<PathBuf>,
#[arg(short, long, global = true)]
pub yes: bool,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(long, global = true, env = "PINNER_NO_CACHE")]
pub no_cache: bool,
#[arg(long, global = true, env = "PINNER_OFFLINE")]
pub offline: bool,
#[arg(short, long, global = true)]
pub dry_run: bool,
#[arg(long, global = true, env = "GITHUB_TOKEN")]
pub github_token: Option<String>,
#[arg(long, global = true, env = "BITBUCKET_TOKEN")]
pub bitbucket_token: Option<String>,
#[arg(long, global = true, env = "GITLAB_TOKEN")]
pub gitlab_token: Option<String>,
#[arg(long, global = true, env = "FORGEJO_TOKEN")]
pub forgejo_token: Option<String>,
#[arg(long, global = true, env = "CIRCLECI_TOKEN")]
pub circleci_token: Option<String>,
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
#[arg(long, global = true)]
pub json: bool,
#[arg(
long,
global = true,
env = "PINNER_GITHUB_URL",
default_value = "https://api.github.com"
)]
pub github_url: String,
#[arg(
long,
global = true,
env = "PINNER_BITBUCKET_URL",
default_value = "https://api.bitbucket.org/2.0"
)]
pub bitbucket_url: String,
#[arg(
long,
global = true,
env = "PINNER_GITLAB_URL",
default_value = "https://gitlab.com"
)]
pub gitlab_url: String,
#[arg(
long,
global = true,
env = "PINNER_FORGEJO_URL",
default_value = "https://codeberg.org"
)]
pub forgejo_url: String,
#[arg(
long,
global = true,
env = "PINNER_CIRCLECI_URL",
default_value = "https://circleci.com/graphql-unstable"
)]
pub circleci_url: String,
#[arg(
long,
global = true,
env = "PINNER_UPGRADE_STRATEGY",
default_value = "latest"
)]
pub upgrade_strategy: UpgradeStrategy,
#[arg(long, global = true, env = "PINNER_CONCURRENCY")]
pub concurrency: Option<usize>,
#[arg(long, global = true, env = "PINNER_IGNORE", value_delimiter = ',')]
pub ignore: Vec<String>,
#[arg(long, global = true, env = "PINNER_OCI_USERNAME")]
pub oci_username: Option<String>,
#[arg(long, global = true, env = "PINNER_OCI_PASSWORD")]
pub oci_password: Option<String>,
}
impl Cli {
pub fn quiet(&self) -> bool {
self.quiet
}
pub fn output_format(&self) -> OutputFormat {
if self.json {
OutputFormat::Json
} else {
self.format.clone()
}
}
}
#[derive(Subcommand, Debug, PartialEq, Clone)]
pub enum Commands {
Pin,
Upgrade {
#[arg(short, long)]
interactive: bool,
},
Verify,
Set {
action: String,
hash: String,
},
InstallHook,
Init,
ExportSbom,
Scan,
GenerateCompletion {
shell: Option<clap_complete::Shell>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_cli_pin_basic() {
let cli = Cli::try_parse_from(["pinner", "pin"]).unwrap();
assert_eq!(cli.command, Commands::Pin);
assert!(!cli.yes);
assert!(!cli.quiet());
assert!(!cli.dry_run);
assert!(!cli.json);
}
#[test]
fn test_cli_verify() {
let cli = Cli::try_parse_from(["pinner", "verify"]).unwrap();
assert_eq!(cli.command, Commands::Verify);
}
#[test]
fn test_cli_flags() {
let cli =
Cli::try_parse_from(["pinner", "-y", "-q", "--dry-run", "--json", "pin"]).unwrap();
assert_eq!(cli.command, Commands::Pin);
assert!(cli.yes);
assert!(cli.quiet);
assert!(cli.dry_run);
assert!(cli.json);
}
#[test]
fn test_cli_upgrade() {
let cli = Cli::try_parse_from(["pinner", "upgrade"]).unwrap();
assert_eq!(cli.command, Commands::Upgrade { interactive: false });
}
#[test]
fn test_cli_set() {
let cli = Cli::try_parse_from([
"pinner",
"set",
"actions/checkout",
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
])
.unwrap();
assert_eq!(
cli.command,
Commands::Set {
action: "actions/checkout".into(),
hash: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".into(),
}
);
}
#[test]
fn test_cli_workflows() {
let cli =
Cli::try_parse_from(["pinner", "-w", "dir1", "--workflows", "dir2", "pin"]).unwrap();
assert_eq!(
cli.workflows,
vec![PathBuf::from("dir1"), PathBuf::from("dir2")]
);
}
#[test]
fn test_cli_methods() {
let cli = Cli {
command: Commands::Pin,
workflows: vec![],
yes: false,
quiet: true,
verbose: false,
no_cache: false,
offline: false,
dry_run: false,
json: false,
github_token: None,
bitbucket_token: None,
gitlab_token: None,
forgejo_token: None,
circleci_token: None,
oci_username: None,
oci_password: None,
format: OutputFormat::Text,
github_url: "https://api.github.com".to_string(),
bitbucket_url: "https://api.bitbucket.org/2.0".to_string(),
gitlab_url: "https://gitlab.com".to_string(),
forgejo_url: "https://codeberg.org".to_string(),
circleci_url: "https://circleci.com/graphql-unstable".to_string(),
upgrade_strategy: UpgradeStrategy::Latest,
concurrency: None,
ignore: vec![],
};
assert!(cli.quiet());
assert_eq!(cli.output_format(), OutputFormat::Text);
assert!(!cli.offline);
let mut cli_json = cli.clone();
cli_json.json = true;
assert_eq!(cli_json.output_format(), OutputFormat::Json);
}
#[test]
fn test_cli_offline() {
let cli = Cli::try_parse_from(["pinner", "--offline", "pin"]).unwrap();
assert!(cli.offline);
}
#[test]
#[serial_test::serial]
fn test_cli_token_env() {
std::env::set_var("GITHUB_TOKEN", "test_token");
let cli = Cli::try_parse_from(["pinner", "pin"]).unwrap();
assert_eq!(cli.github_token, Some("test_token".to_string()));
std::env::remove_var("GITHUB_TOKEN");
}
}