ref:74144b362fece80200adaec5e6a072e8ffd69fc3

feat(cli): add CI secrets management commands

SHA: 74144b362fece80200adaec5e6a072e8ffd69fc3
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-15 22:17
Parents: e2d3d0b
1 files changed +71 -0
Type
src/commands/ci.rs +71 −0
@@ -39,6 +39,37 @@
#[arg(long)]
branch: Option<String>,
},
/// List CI secrets (names only, values hidden)
Secrets {
/// Repository (org/repo)
repo: Option<String>,
},
/// Set a CI secret
#[command(name = "set-secret")]
SetSecret {
/// Secret name
#[arg(long)]
name: String,
/// Secret value
#[arg(long)]
value: String,
/// Environment scope (optional)
#[arg(long)]
env: Option<String>,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Delete a CI secret
#[command(name = "delete-secret")]
DeleteSecret {
/// Secret name
#[arg(long)]
name: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
}
#[derive(Debug, Deserialize)]
@@ -72,5 +103,8 @@
CiCommand::Run { repo, sha, branch } => {
trigger(repo.as_deref(), sha.as_deref(), branch.as_deref()).await
}
CiCommand::Secrets { repo } => list_secrets(repo.as_deref()).await,
CiCommand::SetSecret { name, value, env, repo } => set_secret(repo.as_deref(), &name, &value, env.as_deref()).await,
CiCommand::DeleteSecret { name, repo } => delete_secret(repo.as_deref(), &name).await,
}
}
@@ -256,5 +290,42 @@
output::detail("Branch", &branch_name);
output::detail("Commit", &commit_sha[..8.min(commit_sha.len())]);
Ok(())
}
async fn list_secrets(repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client.get(&format!("/{org}/{name}/ci/secrets")).await?;
let secrets: Vec<serde_json::Value> = resp.get("secrets").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
output::header(&format!("CI Secrets ({org}/{name})"));
let rows: Vec<Vec<String>> = secrets.iter().map(|s| {
vec![
s.get("name").and_then(|v| v.as_str()).unwrap_or("?").to_string(),
s.get("environment").and_then(|v| v.as_str()).unwrap_or("*").to_string(),
s.get("updated_at").and_then(|v| v.as_str()).map(|t| output::format_time(t)).unwrap_or_default(),
]
}).collect();
output::print_table(&["NAME", "ENVIRONMENT", "UPDATED"], &rows);
Ok(())
}
async fn set_secret(repo: Option<&str>, secret_name: &str, value: &str, env: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let mut payload = serde_json::json!({"name": secret_name, "value": value});
if let Some(e) = env {
payload["environment"] = serde_json::json!(e);
}
let _resp: serde_json::Value = client.post(&format!("/{org}/{name}/ci/secrets"), &payload).await?;
output::success(&format!("Set secret '{secret_name}'"));
Ok(())
}
async fn delete_secret(repo: Option<&str>, secret_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
client.delete_empty(&format!("/{org}/{name}/ci/secrets/{secret_name}")).await?;
output::success(&format!("Deleted secret '{secret_name}'"));
Ok(())
}