#![deny(rustdoc::broken_intra_doc_links)]
#![deny(missing_docs)]
#![deny(unsafe_code)]
use reqwest_middleware::ClientWithMiddleware;
use secrecy::{ExposeSecret, SecretString};
mod buildkite;
mod circleci;
mod github;
mod gitlab;
pub use buildkite::Error as BuildkiteError;
pub use github::Error as GitHubError;
pub use gitlab::Error as GitLabError;
pub struct IdToken(SecretString);
impl IdToken {
pub fn reveal(&self) -> &str {
self.0.expose_secret()
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("GitHub Actions detection error")]
GitHubActions(#[from] GitHubError),
#[error("GitLab CI detection error")]
GitLabCI(#[from] GitLabError),
#[error("Buildkite detection error")]
Buildkite(#[from] buildkite::Error),
#[error("CircleCI detection error")]
CircleCI(#[from] circleci::Error),
}
#[derive(Default)]
struct DetectionState {
client: ClientWithMiddleware,
}
trait DetectionStrategy {
type Error;
fn new(state: &DetectionState) -> Option<Self>
where
Self: Sized;
async fn detect(&self, audience: &str) -> Result<IdToken, Self::Error>;
}
pub struct Detector {
state: DetectionState,
}
impl Detector {
pub fn new() -> Self {
Detector {
state: Default::default(),
}
}
pub fn new_with_client(client: impl Into<ClientWithMiddleware>) -> Self {
Detector {
state: DetectionState {
client: client.into(),
},
}
}
pub async fn detect(&self, audience: &str) -> Result<Option<IdToken>, Error> {
macro_rules! detect {
($detector:path) => {
if let Some(detector) = <$detector>::new(&self.state) {
detector.detect(audience).await.map_err(Into::into).map(Some)
} else {
Ok(None)
}
};
($detector:path, $($rest:path),+) => {
if let Some(detector) = <$detector>::new(&self.state) {
detector.detect(audience).await.map_err(Into::into).map(Some)
} else {
detect!($($rest),+)
}
};
}
detect!(
github::GitHubActions,
gitlab::GitLabCI,
buildkite::Buildkite,
circleci::CircleCI
)
}
}
#[cfg(test)]
mod tests {
use crate::Detector;
enum EnvDelta {
Add(String, String),
Remove(String),
}
pub(crate) struct EnvScope {
changes: Vec<EnvDelta>,
}
impl EnvScope {
pub fn new() -> Self {
EnvScope { changes: vec![] }
}
#[allow(unsafe_code)]
pub fn setenv(&mut self, key: &str, value: &str) {
match std::env::var(key) {
Ok(old) => self.changes.push(EnvDelta::Add(key.to_string(), old)),
Err(_) => self.changes.push(EnvDelta::Remove(key.to_string())),
}
unsafe { std::env::set_var(key, value) };
}
#[allow(unsafe_code)]
pub fn unsetenv(&mut self, key: &str) {
match std::env::var(key) {
Ok(old) => self.changes.push(EnvDelta::Add(key.to_string(), old)),
Err(_) => {}
}
unsafe { std::env::remove_var(key) };
}
}
impl Drop for EnvScope {
#[allow(unsafe_code)]
fn drop(&mut self) {
for change in self.changes.drain(..).rev() {
match change {
EnvDelta::Add(key, value) => unsafe { std::env::set_var(key, value) },
EnvDelta::Remove(key) => unsafe { std::env::remove_var(key) },
}
}
}
}
#[tokio::test]
async fn test_no_detection() {
let mut scope = EnvScope::new();
scope.unsetenv("GITHUB_ACTIONS");
scope.unsetenv("GITLAB_CI");
scope.unsetenv("BUILDKITE");
scope.unsetenv("CIRCLECI");
let detector = Detector::new();
assert!(
detector
.detect("bupkis")
.await
.expect("should not error")
.is_none()
);
}
}