Skip to main content

wsb_github/
token.rs

1//! Where the GitHub token comes from.
2//!
3//! The GitHub CLI's own login first — `gh auth token`, which reads the token
4//! `gh auth login` stored in the operating system's credential store — and
5//! then `GITHUB_TOKEN` in the environment. Nothing else is consulted, and no
6//! token is ever written anywhere by this crate: one place to log in, and one
7//! answer to "which token is this using".
8
9use std::fmt;
10use std::process::Command;
11
12/// Which of the two places answered.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Source {
15    /// `gh auth token`.
16    GhLogin,
17    /// The `GITHUB_TOKEN` environment variable.
18    Environment,
19}
20
21impl fmt::Display for Source {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.write_str(match self {
24            Self::GhLogin => "the GitHub CLI's login (gh auth token)",
25            Self::Environment => "GITHUB_TOKEN",
26        })
27    }
28}
29
30/// A token, and where it came from. Its value is never printed.
31#[derive(Clone)]
32pub struct Token {
33    value: String,
34    pub source: Source,
35}
36
37impl Token {
38    pub fn value(&self) -> &str {
39        &self.value
40    }
41}
42
43impl fmt::Debug for Token {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.debug_struct("Token").field("source", &self.source).finish_non_exhaustive()
46    }
47}
48
49/// Why no token could be had: both places, and what each said.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct NoToken {
52    pub gh: String,
53}
54
55impl fmt::Display for NoToken {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(
58            f,
59            "no GitHub token: {}, and GITHUB_TOKEN is not set. Log in with `gh auth login` \
60             (with the `project` scope for boards: `gh auth refresh -s project`), or set GITHUB_TOKEN",
61            self.gh
62        )
63    }
64}
65
66impl std::error::Error for NoToken {}
67
68/// The token to use, from the GitHub CLI's login or else the environment.
69pub fn resolve() -> Result<Token, NoToken> {
70    let gh = match Command::new("gh").args(["auth", "token"]).output() {
71        Ok(out) if out.status.success() => {
72            let value = String::from_utf8_lossy(&out.stdout).trim().to_string();
73            if !value.is_empty() {
74                return Ok(Token {
75                    value,
76                    source: Source::GhLogin,
77                });
78            }
79            "the GitHub CLI answered with no token".to_string()
80        }
81        Ok(out) => format!(
82            "the GitHub CLI has no login ({})",
83            String::from_utf8_lossy(&out.stderr).trim()
84        ),
85        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
86            "the GitHub CLI (gh) is not installed".to_string()
87        }
88        Err(error) => format!("the GitHub CLI could not be run ({error})"),
89    };
90    match std::env::var("GITHUB_TOKEN") {
91        Ok(value) if !value.trim().is_empty() => Ok(Token {
92            value: value.trim().to_string(),
93            source: Source::Environment,
94        }),
95        _ => Err(NoToken { gh }),
96    }
97}