Skip to main content

gor/cmd/
variable.rs

1//! Implementation of the `gor variable` subcommand.
2//!
3//! Provides variable listing and creation for GitHub Actions.
4
5#![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
13/// Run the `gor variable` subcommand.
14///
15/// # Errors
16///
17/// Returns an error if the command execution fails.
18pub 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}