|
| 1 | +use std::env; |
1 | 2 | use std::fs; |
2 | 3 | use std::path::{Path, PathBuf}; |
3 | 4 |
|
@@ -31,10 +32,46 @@ lazy_static! { |
31 | 32 |
|
32 | 33 | const GITHUB_BASE_URL: &str = "https://api.github.com"; |
33 | 34 |
|
| 35 | +/// Resolves a GitHub token from either environment variables or config value. |
| 36 | +/// |
| 37 | +/// If a config token is provided and not empty, returns the config token as-is. |
| 38 | +/// If no config token is provided or it's empty, we check standard GitHub environment variables |
| 39 | +/// in order of precedence as per https://cli.github.com/manual/gh_help_environment: |
| 40 | +/// GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN. |
| 41 | +/// If none are found, returns an error. |
| 42 | +fn resolve_github_token<F>(config_token: Option<&str>, env_lookup: F) -> Result<String> |
| 43 | +where |
| 44 | + F: for<'a> Fn(&'a str) -> Result<String, std::env::VarError>, |
| 45 | +{ |
| 46 | + if let Some(token) = config_token { |
| 47 | + if !token.is_empty() { |
| 48 | + return Ok(token.to_string()); |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + let known_env_vars = [ |
| 53 | + "GH_TOKEN", |
| 54 | + "GITHUB_TOKEN", |
| 55 | + "GH_ENTERPRISE_TOKEN", |
| 56 | + "GITHUB_ENTERPRISE_TOKEN", |
| 57 | + ]; |
| 58 | + |
| 59 | + for env_var in &known_env_vars { |
| 60 | + if let Ok(token) = env_lookup(env_var) { |
| 61 | + if token.is_empty() { |
| 62 | + bail!("Environment variable '{}' located but is empty", env_var); |
| 63 | + } |
| 64 | + return Ok(token); |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + bail!("No GitHub token found in config or environment variables") |
| 69 | +} |
| 70 | + |
34 | 71 | #[derive(Debug, Deserialize)] |
35 | 72 | struct PrrConfig { |
36 | 73 | /// GH personal token |
37 | | - token: String, |
| 74 | + token: Option<String>, |
38 | 75 | /// Directory to place review files |
39 | 76 | workdir: Option<String>, |
40 | 77 | /// Github URL |
@@ -123,8 +160,11 @@ impl Prr { |
123 | 160 | } |
124 | 161 | }; |
125 | 162 |
|
| 163 | + let token = resolve_github_token(config.prr.token.as_deref(), |var| env::var(var)) |
| 164 | + .context("Failed to locate GitHub token")?; |
| 165 | + |
126 | 166 | let octocrab = Octocrab::builder() |
127 | | - .personal_token(config.prr.token.clone()) |
| 167 | + .personal_token(token) |
128 | 168 | .base_uri(config.url()) |
129 | 169 | .context("Failed to parse github base URL")? |
130 | 170 | .build() |
@@ -702,6 +742,88 @@ mod tests { |
702 | 742 | } |
703 | 743 | } |
704 | 744 |
|
| 745 | + #[test] |
| 746 | + fn test_resolve_github_token_with_no_config_token_fallback_to_env() { |
| 747 | + let env_lookup = |var: &str| -> Result<String, std::env::VarError> { |
| 748 | + match var { |
| 749 | + "GITHUB_TOKEN" => Ok("fallback_env_token".to_string()), |
| 750 | + _ => Err(std::env::VarError::NotPresent), |
| 751 | + } |
| 752 | + }; |
| 753 | + |
| 754 | + let result = resolve_github_token(None, env_lookup).unwrap(); |
| 755 | + assert_eq!(result, "fallback_env_token"); |
| 756 | + } |
| 757 | + |
| 758 | + #[test] |
| 759 | + fn test_resolve_github_token_with_no_config_token_no_env_error() { |
| 760 | + let env_lookup = |_var: &str| -> Result<String, std::env::VarError> { |
| 761 | + Err(std::env::VarError::NotPresent) |
| 762 | + }; |
| 763 | + |
| 764 | + let result = resolve_github_token(None, env_lookup); |
| 765 | + assert!(result.is_err()); |
| 766 | + let error_msg = result.err().unwrap().to_string(); |
| 767 | + assert!(error_msg.contains("No GitHub token found in config or environment variables")); |
| 768 | + } |
| 769 | + |
| 770 | + #[test] |
| 771 | + fn test_resolve_github_token_config_token_preferred_over_env() { |
| 772 | + let env_lookup = |var: &str| -> Result<String, std::env::VarError> { |
| 773 | + match var { |
| 774 | + "GITHUB_TOKEN" => Ok("env_token".to_string()), |
| 775 | + _ => Err(std::env::VarError::NotPresent), |
| 776 | + } |
| 777 | + }; |
| 778 | + |
| 779 | + let result = resolve_github_token(Some("config_token"), env_lookup).unwrap(); |
| 780 | + assert_eq!(result, "config_token"); |
| 781 | + } |
| 782 | + |
| 783 | + #[test] |
| 784 | + fn test_resolve_github_token_empty_config_token_falls_back_to_env() { |
| 785 | + let env_lookup = |var: &str| -> Result<String, std::env::VarError> { |
| 786 | + match var { |
| 787 | + "GITHUB_TOKEN" => Ok("env_token".to_string()), |
| 788 | + _ => Err(std::env::VarError::NotPresent), |
| 789 | + } |
| 790 | + }; |
| 791 | + |
| 792 | + let result = resolve_github_token(Some(""), env_lookup).unwrap(); |
| 793 | + assert_eq!(result, "env_token"); |
| 794 | + } |
| 795 | + |
| 796 | + #[test] |
| 797 | + fn test_resolve_github_token_env_var_precedence() { |
| 798 | + let env_lookup = |var: &str| -> Result<String, std::env::VarError> { |
| 799 | + match var { |
| 800 | + "GH_TOKEN" => Ok("gh_token".to_string()), |
| 801 | + "GITHUB_TOKEN" => Ok("github_token".to_string()), |
| 802 | + _ => Err(std::env::VarError::NotPresent), |
| 803 | + } |
| 804 | + }; |
| 805 | + |
| 806 | + let result = resolve_github_token(None, env_lookup).unwrap(); |
| 807 | + // GH_TOKEN should have higher precedence |
| 808 | + assert_eq!(result, "gh_token"); |
| 809 | + } |
| 810 | + |
| 811 | + #[test] |
| 812 | + fn test_resolve_github_token_empty_env_var_error() { |
| 813 | + let env_lookup = |var: &str| -> Result<String, std::env::VarError> { |
| 814 | + match var { |
| 815 | + // Empty token |
| 816 | + "GITHUB_TOKEN" => Ok("".to_string()), |
| 817 | + _ => Err(std::env::VarError::NotPresent), |
| 818 | + } |
| 819 | + }; |
| 820 | + |
| 821 | + let result = resolve_github_token(None, env_lookup); |
| 822 | + assert!(result.is_err()); |
| 823 | + let error_msg = result.err().unwrap().to_string(); |
| 824 | + assert!(error_msg.contains("Environment variable 'GITHUB_TOKEN' located but is empty")); |
| 825 | + } |
| 826 | + |
705 | 827 | #[tokio::test] |
706 | 828 | async fn test_apply_pr() { |
707 | 829 | let gconfig = r#" |
|
0 commit comments