1#![deny(rustdoc::broken_intra_doc_links)]
21#![deny(missing_docs)]
22#![deny(unsafe_code)]
23
24use reqwest_middleware::ClientWithMiddleware;
25use secrecy::{ExposeSecret, SecretString};
26
27mod buildkite;
28mod circleci;
29mod github;
30mod gitlab;
31
32pub use buildkite::Error as BuildkiteError;
33pub use github::Error as GitHubError;
34pub use gitlab::Error as GitLabError;
35
36pub struct IdToken(SecretString);
43
44impl IdToken {
45 pub fn reveal(&self) -> &str {
50 self.0.expose_secret()
51 }
52}
53
54#[derive(Debug, thiserror::Error)]
56pub enum Error {
57 #[error("GitHub Actions detection error")]
59 GitHubActions(#[from] GitHubError),
60 #[error("GitLab CI detection error")]
62 GitLabCI(#[from] GitLabError),
63 #[error("Buildkite detection error")]
65 Buildkite(#[from] buildkite::Error),
66 #[error("CircleCI detection error")]
68 CircleCI(#[from] circleci::Error),
69}
70
71#[derive(Default)]
72struct DetectionState {
73 client: ClientWithMiddleware,
74}
75
76trait DetectionStrategy {
78 type Error;
79
80 fn new(state: &DetectionState) -> Option<Self>
81 where
82 Self: Sized;
83
84 async fn detect(&self, audience: &str) -> Result<IdToken, Self::Error>;
85}
86
87pub struct Detector {
89 state: DetectionState,
90}
91
92impl Detector {
93 pub fn new() -> Self {
95 Detector {
96 state: Default::default(),
97 }
98 }
99
100 pub fn new_with_client(client: impl Into<ClientWithMiddleware>) -> Self {
102 Detector {
103 state: DetectionState {
104 client: client.into(),
105 },
106 }
107 }
108
109 pub async fn detect(&self, audience: &str) -> Result<Option<IdToken>, Error> {
119 macro_rules! detect {
120 ($detector:path) => {
121 if let Some(detector) = <$detector>::new(&self.state) {
122 detector.detect(audience).await.map_err(Into::into).map(Some)
123 } else {
124 Ok(None)
125 }
126 };
127 ($detector:path, $($rest:path),+) => {
128 if let Some(detector) = <$detector>::new(&self.state) {
129 detector.detect(audience).await.map_err(Into::into).map(Some)
130 } else {
131 detect!($($rest),+)
132 }
133 };
134 }
135
136 detect!(
137 github::GitHubActions,
138 gitlab::GitLabCI,
139 buildkite::Buildkite,
140 circleci::CircleCI
141 )
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use crate::Detector;
148
149 enum EnvDelta {
151 Add(String, String),
153 Remove(String),
155 }
156
157 pub(crate) struct EnvScope {
162 changes: Vec<EnvDelta>,
163 }
164
165 impl EnvScope {
166 pub fn new() -> Self {
167 EnvScope { changes: vec![] }
168 }
169
170 #[allow(unsafe_code)]
172 pub fn setenv(&mut self, key: &str, value: &str) {
173 match std::env::var(key) {
174 Ok(old) => self.changes.push(EnvDelta::Add(key.to_string(), old)),
176 Err(_) => self.changes.push(EnvDelta::Remove(key.to_string())),
178 }
179
180 unsafe { std::env::set_var(key, value) };
181 }
182
183 #[allow(unsafe_code)]
185 pub fn unsetenv(&mut self, key: &str) {
186 match std::env::var(key) {
187 Ok(old) => self.changes.push(EnvDelta::Add(key.to_string(), old)),
189 Err(_) => {}
191 }
192
193 unsafe { std::env::remove_var(key) };
194 }
195 }
196
197 impl Drop for EnvScope {
198 #[allow(unsafe_code)]
199 fn drop(&mut self) {
200 for change in self.changes.drain(..).rev() {
202 match change {
203 EnvDelta::Add(key, value) => unsafe { std::env::set_var(key, value) },
204 EnvDelta::Remove(key) => unsafe { std::env::remove_var(key) },
205 }
206 }
207 }
208 }
209
210 #[tokio::test]
211 async fn test_no_detection() {
212 let mut scope = EnvScope::new();
213 scope.unsetenv("GITHUB_ACTIONS");
214 scope.unsetenv("GITLAB_CI");
215 scope.unsetenv("BUILDKITE");
216 scope.unsetenv("CIRCLECI");
217
218 let detector = Detector::new();
219
220 assert!(
221 detector
222 .detect("bupkis")
223 .await
224 .expect("should not error")
225 .is_none()
226 );
227 }
228}