Skip to main content

flowey_lib_common/
use_gh_cli.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Set up `gh` CLI for use with flowey.
5//!
6//! The executable this node returns will wrap the base `gh` cli executable with
7//! some additional logic, notably, ensuring it is includes any necessary
8//! authentication.
9
10use flowey::node::prelude::*;
11use std::io::Write;
12
13/// Auth config for the gh CLI node. Uses [`ConfigVar`] so that
14/// `PartialEq`-based config merging works for the `ReadVar` variant.
15#[derive(Serialize, Deserialize, Clone, PartialEq, Default)]
16pub enum GhCliAuth {
17    /// Prompt user to log-in interactively.
18    #[default]
19    LocalOnlyInteractive,
20    /// Set the value of the `GITHUB_TOKEN` environment variable.
21    AuthToken(ConfigVar<String>),
22}
23
24#[derive(Serialize, Deserialize)]
25#[doc(hidden)]
26pub enum ClaimedGhCliAuth {
27    LocalOnlyInteractive,
28    AuthToken(ClaimedReadVar<String>),
29}
30
31impl ClaimVar for GhCliAuth {
32    type Claimed = ClaimedGhCliAuth;
33
34    fn claim(self, ctx: &mut StepCtx<'_>) -> Self::Claimed {
35        match self {
36            GhCliAuth::LocalOnlyInteractive => ClaimedGhCliAuth::LocalOnlyInteractive,
37            GhCliAuth::AuthToken(v) => ClaimedGhCliAuth::AuthToken(v.claim(ctx)),
38        }
39    }
40}
41
42flowey_config! {
43    /// Config for the use_gh_cli node.
44    pub struct Config {
45        /// Specify what authentication to use
46        pub auth: Option<GhCliAuth>,
47    }
48}
49
50flowey_request! {
51    pub enum Request {
52        /// Get a path to `gh` executable
53        Get(WriteVar<PathBuf>),
54    }
55}
56
57new_flow_node_with_config!(struct Node);
58
59impl FlowNodeWithConfig for Node {
60    type Request = Request;
61    type Config = Config;
62
63    fn imports(ctx: &mut ImportCtx<'_>) {
64        ctx.import::<crate::download_gh_cli::Node>();
65    }
66
67    fn emit(
68        config: Config,
69        requests: Vec<Self::Request>,
70        ctx: &mut NodeCtx<'_>,
71    ) -> anyhow::Result<()> {
72        let mut get_reqs = Vec::new();
73
74        for req in requests {
75            match req {
76                Request::Get(v) => get_reqs.push(v),
77            }
78        }
79
80        let auth = config.auth.ok_or(anyhow::anyhow!("missing config: auth"))?;
81        let get_reqs = get_reqs;
82
83        // -- end of req processing -- //
84
85        if get_reqs.is_empty() {
86            if let GhCliAuth::AuthToken(tok) = auth {
87                tok.0.claim_unused(ctx);
88            }
89            return Ok(());
90        }
91
92        if !matches!(ctx.backend(), FlowBackend::Local) {
93            if matches!(auth, GhCliAuth::LocalOnlyInteractive) {
94                anyhow::bail!("cannot use interactive auth on a non-local backend")
95            }
96        }
97
98        let gh_bin_path = ctx.reqv(crate::download_gh_cli::Request::Get);
99
100        ctx.emit_rust_step("setup gh cli", |ctx| {
101            let auth = auth.claim(ctx);
102            let get_reqs = get_reqs.claim(ctx);
103            let gh_bin_path = gh_bin_path.claim(ctx);
104            |rt| {
105                let gh_bin_path = rt.read(gh_bin_path).display().to_string();
106                let gh_token = match auth {
107                    ClaimedGhCliAuth::LocalOnlyInteractive => String::new(),
108                    ClaimedGhCliAuth::AuthToken(tok) => rt.read(tok),
109                };
110                // only set GITHUB_TOKEN if there is a value to set it to, otherwise
111                // let the user's environment take precedence over authenticating interactively
112                let gh_token = if !gh_token.is_empty() {
113                    match rt.platform().kind() {
114                        FlowPlatformKind::Windows => format!(r#"SET "GITHUB_TOKEN={gh_token}""#),
115                        FlowPlatformKind::Unix => format!(r#"GITHUB_TOKEN="{gh_token}""#),
116                    }
117                } else {
118                    String::new()
119                };
120
121                let shim_txt = match rt.platform().kind() {
122                    FlowPlatformKind::Windows => WINDOWS_SHIM_BAT.trim(),
123                    FlowPlatformKind::Unix => UNIX_SHIM_SH.trim(),
124                }
125                .replace("{GITHUB_TOKEN}", &gh_token)
126                .replace("{GH_BIN_PATH}", &gh_bin_path);
127
128                let script_name = match rt.platform().kind() {
129                    FlowPlatformKind::Windows => "shim.bat",
130                    FlowPlatformKind::Unix => "shim.sh",
131                };
132                let path = {
133                    let dst = std::env::current_dir()?.join(script_name);
134                    let mut options = fs_err::OpenOptions::new();
135                    #[cfg(unix)]
136                    fs_err::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o777); // executable
137                    let mut file = options.create_new(true).write(true).open(&dst)?;
138                    file.write_all(shim_txt.as_bytes())?;
139                    dst.absolute()?
140                };
141                if !flowey::shell_cmd!(rt, "{path} auth status")
142                    .ignore_status()
143                    .output()?
144                    .status
145                    .success()
146                {
147                    if matches!(rt.backend(), FlowBackend::Local) {
148                        flowey::shell_cmd!(rt, "{path} auth login").run()?;
149                    } else {
150                        anyhow::bail!("unable to authenticate with github - is GhCliAuth valid?")
151                    }
152                };
153
154                for var in get_reqs {
155                    rt.write(var, &path);
156                }
157
158                Ok(())
159            }
160        });
161
162        Ok(())
163    }
164}
165
166const UNIX_SHIM_SH: &str = r#"
167#!/bin/sh
168{GITHUB_TOKEN} exec {GH_BIN_PATH} "$@"
169"#;
170
171const WINDOWS_SHIM_BAT: &str = r#"
172@ECHO OFF
173{GITHUB_TOKEN}
174{GH_BIN_PATH} %*
175"#;