1#![allow(clippy::print_stdout)]
6
7use crate::cli::SecretCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use crate::repository::detect_remote;
11use anyhow::Context;
12
13pub fn run(cmd: SecretCommand) -> anyhow::Result<()> {
19 match cmd {
20 SecretCommand::List {
21 org,
22 env,
23 json,
24 hostname,
25 } => list(org.as_deref(), env.as_deref(), json, hostname.as_deref()),
26 SecretCommand::Set {
27 name,
28 body,
29 file,
30 org,
31 env,
32 hostname,
33 } => set(
34 &name,
35 body.as_deref(),
36 file.as_deref(),
37 org.as_deref(),
38 env.as_deref(),
39 hostname.as_deref(),
40 ),
41 SecretCommand::Delete {
42 name,
43 org,
44 env,
45 hostname,
46 } => delete(&name, org.as_deref(), env.as_deref(), hostname.as_deref()),
47 }
48}
49
50fn list(
51 org: Option<&str>,
52 env: Option<&str>,
53 json: Option<Vec<String>>,
54 hostname: Option<&str>,
55) -> anyhow::Result<()> {
56 let host = hostname.unwrap_or("github.com");
57 let client = Client::new(host).context("failed to create HTTP client")?;
58
59 let path = if let Some(o) = org {
60 format!("/orgs/{o}/actions/secrets?per_page=100")
61 } else if let Some(e) = env {
62 let spec = detect_remote().ok_or_else(|| {
63 anyhow::anyhow!(
64 "could not detect repository; specify --repo or run from a repo directory"
65 )
66 })?;
67 format!(
68 "/repos/{}/{}/environments/{e}/secrets?per_page=100",
69 spec.owner, spec.repo
70 )
71 } else {
72 let spec = detect_remote().ok_or_else(|| {
73 anyhow::anyhow!(
74 "could not detect repository; specify --org or run from a repo directory"
75 )
76 })?;
77 format!(
78 "/repos/{}/{}/actions/secrets?per_page=100",
79 spec.owner, spec.repo
80 )
81 };
82
83 let response = client.get(&path).context("failed to fetch secrets")?;
84 let status = response.status();
85 if !status.is_success() {
86 anyhow::bail!("failed to list secrets: HTTP {status}");
87 }
88
89 let result: serde_json::Value = response.json().context("failed to parse response")?;
90 let secrets: Vec<serde_json::Value> = result["secrets"]
91 .as_array()
92 .map_or_else(Vec::new, Clone::clone);
93
94 if let Some(fields) = json {
95 let fields_ref: Option<&[String]> = if fields.is_empty() {
96 None
97 } else {
98 Some(&fields)
99 };
100 print_json(&secrets, fields_ref);
101 return Ok(());
102 }
103
104 if secrets.is_empty() {
105 println!("No secrets found.");
106 return Ok(());
107 }
108
109 println!("{:<30} UPDATED", "NAME");
110 for s in &secrets {
111 let name = s["name"].as_str().unwrap_or("—");
112 let updated = s["updated_at"].as_str().map_or("—", |d| d);
113 let name_truncated = crate::cmd::util::truncate(name, 30);
114 println!("{name_truncated:<30} {updated}");
115 }
116
117 Ok(())
118}
119
120fn set(
121 name: &str,
122 body: Option<&str>,
123 file: Option<&str>,
124 org: Option<&str>,
125 env: Option<&str>,
126 hostname: Option<&str>,
127) -> anyhow::Result<()> {
128 let host = hostname.unwrap_or("github.com");
129 let client = Client::new(host).context("failed to create HTTP client")?;
130
131 let value = if let Some(b) = body {
132 b.to_string()
133 } else if let Some(f) = file {
134 std::fs::read_to_string(f)
135 .with_context(|| format!("failed to read file: {f}"))?
136 .trim()
137 .to_string()
138 } else {
139 anyhow::bail!("no secret value provided (use --body or --file)");
140 };
141
142 let body_value = serde_json::json!({"encrypted_value": value});
143
144 let path = if let Some(o) = org {
145 format!("/orgs/{o}/actions/secrets/{name}")
146 } else if let Some(e) = env {
147 let spec = detect_remote().ok_or_else(|| {
148 anyhow::anyhow!(
149 "could not detect repository; specify --repo or run from a repo directory"
150 )
151 })?;
152 format!(
153 "/repos/{}/{}/environments/{e}/secrets/{name}",
154 spec.owner, spec.repo
155 )
156 } else {
157 let spec = detect_remote().ok_or_else(|| {
158 anyhow::anyhow!(
159 "could not detect repository; specify --org or run from a repo directory"
160 )
161 })?;
162 format!("/repos/{}/{}/actions/secrets/{name}", spec.owner, spec.repo)
163 };
164
165 let response = client
166 .request(
167 "PUT",
168 &path,
169 &[],
170 Some(serde_json::to_vec(&body_value).context("serialize")?),
171 )
172 .context("failed to set secret")?;
173
174 let status = response.status();
175 if !status.is_success() {
176 anyhow::bail!("failed to set secret '{name}': HTTP {status}");
177 }
178
179 println!("Secret '{name}' set.");
180 Ok(())
181}
182
183fn delete(
184 name: &str,
185 org: Option<&str>,
186 env: Option<&str>,
187 hostname: Option<&str>,
188) -> anyhow::Result<()> {
189 let host = hostname.unwrap_or("github.com");
190 let client = Client::new(host).context("failed to create HTTP client")?;
191
192 let path = if let Some(o) = org {
193 format!("/orgs/{o}/actions/secrets/{name}")
194 } else if let Some(e) = env {
195 let spec = detect_remote().ok_or_else(|| {
196 anyhow::anyhow!(
197 "could not detect repository; specify --repo or run from a repo directory"
198 )
199 })?;
200 format!(
201 "/repos/{}/{}/environments/{e}/secrets/{name}",
202 spec.owner, spec.repo
203 )
204 } else {
205 let spec = detect_remote().ok_or_else(|| {
206 anyhow::anyhow!(
207 "could not detect repository; specify --org or run from a repo directory"
208 )
209 })?;
210 format!("/repos/{}/{}/actions/secrets/{name}", spec.owner, spec.repo)
211 };
212
213 let response = client
214 .request("DELETE", &path, &[], None)
215 .context("failed to delete secret")?;
216
217 let status = response.status();
218 if status == 404 {
219 anyhow::bail!("secret '{name}' not found");
220 }
221 if !status.is_success() {
222 anyhow::bail!("failed to delete secret '{name}': HTTP {status}");
223 }
224
225 println!("Secret '{name}' deleted.");
226 Ok(())
227}