1#![allow(clippy::print_stdout)]
6
7use crate::cli::VariableCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use crate::repository::detect_remote;
11use anyhow::Context;
12
13pub fn run(cmd: VariableCommand) -> anyhow::Result<()> {
19 match cmd {
20 VariableCommand::List {
21 org,
22 env,
23 json,
24 hostname,
25 } => list(org.as_deref(), env.as_deref(), json, hostname.as_deref()),
26 VariableCommand::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 VariableCommand::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/variables?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}/variables?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/variables?per_page=100",
79 spec.owner, spec.repo
80 )
81 };
82
83 let response = client.get(&path).context("failed to fetch variables")?;
84 let status = response.status();
85 if !status.is_success() {
86 anyhow::bail!("failed to list variables: HTTP {status}");
87 }
88
89 let result: serde_json::Value = response.json().context("failed to parse response")?;
90 let vars: Vec<serde_json::Value> = result["variables"]
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(&vars, fields_ref);
101 return Ok(());
102 }
103
104 if vars.is_empty() {
105 println!("No variables found.");
106 return Ok(());
107 }
108
109 println!("{:<30} VALUE", "NAME");
110 for v in &vars {
111 let name = v["name"].as_str().unwrap_or("—");
112 let value = v["value"].as_str().unwrap_or("—");
113 let name_truncated = crate::cmd::util::truncate(name, 30);
114 let value_truncated = crate::cmd::util::truncate(value, 40);
115 println!("{name_truncated:<30} {value_truncated}");
116 }
117
118 Ok(())
119}
120
121fn delete(
122 name: &str,
123 org: Option<&str>,
124 env: Option<&str>,
125 hostname: Option<&str>,
126) -> anyhow::Result<()> {
127 let host = hostname.unwrap_or("github.com");
128 let client = Client::new(host).context("failed to create HTTP client")?;
129
130 let path = if let Some(o) = org {
131 format!("/orgs/{o}/actions/variables/{name}")
132 } else if let Some(e) = env {
133 let spec = detect_remote().ok_or_else(|| {
134 anyhow::anyhow!(
135 "could not detect repository; specify --repo or run from a repo directory"
136 )
137 })?;
138 format!(
139 "/repos/{}/{}/environments/{e}/variables/{name}",
140 spec.owner, spec.repo
141 )
142 } else {
143 let spec = detect_remote().ok_or_else(|| {
144 anyhow::anyhow!(
145 "could not detect repository; specify --org or run from a repo directory"
146 )
147 })?;
148 format!(
149 "/repos/{}/{}/actions/variables/{name}",
150 spec.owner, spec.repo
151 )
152 };
153
154 let response = client
155 .request("DELETE", &path, &[], None)
156 .context("failed to delete variable")?;
157
158 let status = response.status();
159 if status == 404 {
160 anyhow::bail!("variable '{name}' not found");
161 }
162 if !status.is_success() {
163 anyhow::bail!("failed to delete variable '{name}': HTTP {status}");
164 }
165
166 println!("Variable '{name}' deleted.");
167 Ok(())
168}
169
170fn set(
171 name: &str,
172 body: Option<&str>,
173 file: Option<&str>,
174 org: Option<&str>,
175 env: Option<&str>,
176 hostname: Option<&str>,
177) -> anyhow::Result<()> {
178 let host = hostname.unwrap_or("github.com");
179 let client = Client::new(host).context("failed to create HTTP client")?;
180
181 let value = if let Some(b) = body {
182 b.to_string()
183 } else if let Some(f) = file {
184 std::fs::read_to_string(f)
185 .with_context(|| format!("failed to read file: {f}"))?
186 .trim()
187 .to_string()
188 } else {
189 anyhow::bail!("no variable value provided (use --body or --file)");
190 };
191
192 let body_value = serde_json::json!({"value": value});
193
194 let path = if let Some(o) = org {
195 format!("/orgs/{o}/actions/variables/{name}")
196 } else if let Some(e) = env {
197 let spec = detect_remote().ok_or_else(|| {
198 anyhow::anyhow!(
199 "could not detect repository; specify --repo or run from a repo directory"
200 )
201 })?;
202 format!(
203 "/repos/{}/{}/environments/{e}/variables/{name}",
204 spec.owner, spec.repo
205 )
206 } else {
207 let spec = detect_remote().ok_or_else(|| {
208 anyhow::anyhow!(
209 "could not detect repository; specify --org or run from a repo directory"
210 )
211 })?;
212 format!(
213 "/repos/{}/{}/actions/variables/{name}",
214 spec.owner, spec.repo
215 )
216 };
217
218 let response = client
219 .request(
220 "PATCH",
221 &path,
222 &[],
223 Some(serde_json::to_vec(&body_value).context("serialize")?),
224 )
225 .context("failed to set variable")?;
226
227 let status = response.status();
228 if !status.is_success() {
229 anyhow::bail!("failed to set variable '{name}': HTTP {status}");
230 }
231
232 println!("Variable '{name}' set.");
233 Ok(())
234}