use crate::types::Environment;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunnerEnv {
Local,
Ci,
}
impl RunnerEnv {
pub fn detect() -> Self {
if env_truthy("GITHUB_ACTIONS") || env_truthy("CI") {
RunnerEnv::Ci
} else {
RunnerEnv::Local
}
}
}
fn env_truthy(key: &str) -> bool {
matches!(
std::env::var(key).ok().as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("True"),
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateOutcome {
Allow { warning: Option<String> },
Refuse { reason: String },
}
pub fn evaluate(environment: Environment, env: RunnerEnv, force: bool) -> GateOutcome {
match (environment, env) {
(Environment::Workstation, RunnerEnv::Local)
| (Environment::Any, _)
| (Environment::Ci, RunnerEnv::Ci) => GateOutcome::Allow { warning: None },
(Environment::Workstation, RunnerEnv::Ci) => GateOutcome::Allow {
warning: Some(
"recipe environment = workstation but runner is CI — the run's output \
(installed binary, files in the camp tree, …) is meaningless on a \
CI runner. Consider splitting the recipe or flipping environment to \
`any`."
.to_string(),
),
},
(Environment::Ci, RunnerEnv::Local) => {
if force {
GateOutcome::Allow {
warning: Some(
"recipe environment = ci but --force was passed; running \
locally. Steps that depend on CI secrets or signing identity \
will fail unless your environment already provides them."
.to_string(),
),
}
} else {
GateOutcome::Refuse {
reason: "recipe environment = ci and this is not a CI runner — \
the recipe needs secrets, signing identity, or a clean \
runner that don't exist locally. Pass --force to run \
anyway, or run it from a CI workflow."
.to_string(),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_allowed(p: Environment, e: RunnerEnv, force: bool) {
match evaluate(p, e, force) {
GateOutcome::Allow { .. } => {}
other => panic!("expected Allow for ({p:?}, {e:?}, force={force}); got {other:?}"),
}
}
fn assert_allowed_with_warning(p: Environment, e: RunnerEnv, force: bool) {
match evaluate(p, e, force) {
GateOutcome::Allow { warning: Some(_) } => {}
other => panic!(
"expected Allow {{warning: Some(_)}} for ({p:?}, {e:?}, force={force}); got {other:?}"
),
}
}
fn assert_refused(p: Environment, e: RunnerEnv, force: bool) {
match evaluate(p, e, force) {
GateOutcome::Refuse { .. } => {}
other => panic!("expected Refuse for ({p:?}, {e:?}, force={force}); got {other:?}"),
}
}
#[test]
fn matrix_workstation_on_local_allows_silently() {
assert_allowed(Environment::Workstation, RunnerEnv::Local, false);
}
#[test]
fn matrix_any_on_local_allows_silently() {
assert_allowed(Environment::Any, RunnerEnv::Local, false);
}
#[test]
fn matrix_any_on_ci_allows_silently() {
assert_allowed(Environment::Any, RunnerEnv::Ci, false);
}
#[test]
fn matrix_ci_on_ci_allows_silently() {
assert_allowed(Environment::Ci, RunnerEnv::Ci, false);
}
#[test]
fn matrix_workstation_on_ci_warns_but_allows() {
assert_allowed_with_warning(Environment::Workstation, RunnerEnv::Ci, false);
}
#[test]
fn matrix_ci_on_local_refuses_without_force() {
assert_refused(Environment::Ci, RunnerEnv::Local, false);
}
#[test]
fn matrix_ci_on_local_with_force_allows_with_warning() {
assert_allowed_with_warning(Environment::Ci, RunnerEnv::Local, true);
}
#[test]
fn force_is_no_op_on_already_allowed_cells() {
for p in [Environment::Workstation, Environment::Any, Environment::Ci] {
for e in [RunnerEnv::Local, RunnerEnv::Ci] {
let without = evaluate(p, e, false);
if matches!(without, GateOutcome::Allow { warning: None }) {
let with_force = evaluate(p, e, true);
assert_eq!(
with_force, without,
"force should not change behavior for ({p:?}, {e:?})"
);
}
}
}
}
#[test]
fn env_truthy_recognizes_canonical_values() {
for val in ["1", "true", "TRUE", "True"] {
unsafe { std::env::set_var("__QED_GATE_TEST", val) };
assert!(env_truthy("__QED_GATE_TEST"), "truthy: {val}");
}
for val in ["0", "false", "no", ""] {
unsafe { std::env::set_var("__QED_GATE_TEST", val) };
assert!(!env_truthy("__QED_GATE_TEST"), "not truthy: {val}");
}
unsafe { std::env::remove_var("__QED_GATE_TEST") };
}
}