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#[derive(Serialize, Deserialize)]
14pub enum GhCliAuth<C = VarNotClaimed> {
15    /// Prompt user to log-in interactively.
16    LocalOnlyInteractive,
17    /// Set the value of the `GITHUB_TOKEN` environment variable to the
18    /// specified runtime String when invoking the `gh` CLI.
19    AuthToken(ReadVar<String, C>),
20}
21
22impl ClaimVar for GhCliAuth {
23    type Claimed = GhCliAuth<VarClaimed>;
24
25    fn claim(self, ctx: &mut StepCtx<'_>) -> Self::Claimed {
26        match self {
27            GhCliAuth::LocalOnlyInteractive => GhCliAuth::LocalOnlyInteractive,
28            GhCliAuth::AuthToken(v) => GhCliAuth::AuthToken(v.claim(ctx)),
29        }
30    }
31}
32
33flowey_request! {
34    pub enum Request {
35        /// Specify what authentication to use
36        WithAuth(GhCliAuth),
37        /// Get a path to `gh` executable
38        Get(WriteVar<PathBuf>),
39    }
40}
41
42new_flow_node!(struct Node);
43
44impl FlowNode for Node {
45    type Request = Request;
46
47    fn imports(ctx: &mut ImportCtx<'_>) {
48        ctx.import::<crate::download_gh_cli::Node>();
49    }
50
51    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
52        let mut get_reqs = Vec::new();
53        let mut with_auth_interactive = false;
54        let mut with_auth_token = None;
55
56        for req in requests {
57            match req {
58                Request::WithAuth(v) => match v {
59                    GhCliAuth::LocalOnlyInteractive => with_auth_interactive = true,
60                    GhCliAuth::AuthToken(v) => {
61                        same_across_all_reqs_backing_var("WithAuth", &mut with_auth_token, v)?
62                    }
63                },
64                Request::Get(v) => get_reqs.push(v),
65            }
66        }
67
68        let get_reqs = get_reqs;
69        let auth = match (with_auth_interactive, with_auth_token) {
70            (true, None) => GhCliAuth::LocalOnlyInteractive,
71            (false, Some(v)) => GhCliAuth::AuthToken(v),
72            (true, Some(_)) => {
73                anyhow::bail!("`WithAuth` must be consistent across requests")
74            }
75            (false, None) => anyhow::bail!("Missing essential request: WithAuth"),
76        };
77
78        // -- end of req processing -- //
79
80        if get_reqs.is_empty() {
81            if let GhCliAuth::AuthToken(tok) = auth {
82                tok.claim_unused(ctx);
83            }
84            return Ok(());
85        }
86
87        if !matches!(ctx.backend(), FlowBackend::Local) {
88            if matches!(auth, GhCliAuth::LocalOnlyInteractive) {
89                anyhow::bail!("cannot use interactive auth on a non-local backend")
90            }
91        }
92
93        let gh_bin_path = ctx.reqv(crate::download_gh_cli::Request::Get);
94
95        ctx.emit_rust_step("setup gh cli", |ctx| {
96            let auth = auth.claim(ctx);
97            let get_reqs = get_reqs.claim(ctx);
98            let gh_bin_path = gh_bin_path.claim(ctx);
99            |rt| {
100                let sh = xshell::Shell::new()?;
101
102                let gh_bin_path = rt.read(gh_bin_path).display().to_string();
103                let gh_token = match auth {
104                    GhCliAuth::LocalOnlyInteractive => String::new(),
105                    GhCliAuth::AuthToken(tok) => rt.read(tok),
106                };
107                // only set GITHUB_TOKEN if there is a value to set it to, otherwise
108                // let the user's environment take precedence over authenticating interactively
109                let gh_token = if !gh_token.is_empty() {
110                    match rt.platform().kind() {
111                        FlowPlatformKind::Windows => format!(r#"SET "GITHUB_TOKEN={gh_token}""#),
112                        FlowPlatformKind::Unix => format!(r#"GITHUB_TOKEN="{gh_token}""#),
113                    }
114                } else {
115                    String::new()
116                };
117
118                let shim_txt = match rt.platform().kind() {
119                    FlowPlatformKind::Windows => WINDOWS_SHIM_BAT.trim(),
120                    FlowPlatformKind::Unix => UNIX_SHIM_SH.trim(),
121                }
122                .replace("{GITHUB_TOKEN}", &gh_token)
123                .replace("{GH_BIN_PATH}", &gh_bin_path);
124
125                let script_name = match rt.platform().kind() {
126                    FlowPlatformKind::Windows => "shim.bat",
127                    FlowPlatformKind::Unix => "shim.sh",
128                };
129                let path = {
130                    let dst = std::env::current_dir()?.join(script_name);
131                    let mut options = fs_err::OpenOptions::new();
132                    #[cfg(unix)]
133                    fs_err::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o777); // executable
134                    let mut file = options.create_new(true).write(true).open(&dst)?;
135                    file.write_all(shim_txt.as_bytes())?;
136                    dst.absolute()?
137                };
138                if !xshell::cmd!(sh, "{path} auth status")
139                    .ignore_status()
140                    .output()?
141                    .status
142                    .success()
143                {
144                    if matches!(rt.backend(), FlowBackend::Local) {
145                        xshell::cmd!(sh, "{path} auth login").run()?;
146                    } else {
147                        anyhow::bail!("unable to authenticate with github - is GhCliAuth valid?")
148                    }
149                };
150
151                for var in get_reqs {
152                    rt.write(var, &path);
153                }
154
155                Ok(())
156            }
157        });
158
159        Ok(())
160    }
161}
162
163const UNIX_SHIM_SH: &str = r#"
164#!/bin/sh
165{GITHUB_TOKEN} exec {GH_BIN_PATH} "$@"
166"#;
167
168const WINDOWS_SHIM_BAT: &str = r#"
169@ECHO OFF
170{GITHUB_TOKEN}
171{GH_BIN_PATH} %*
172"#;