use crate::types::Placement;
#[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(placement: Placement, env: RunnerEnv, force: bool) -> GateOutcome {
match (placement, env) {
(Placement::LocalOnly, RunnerEnv::Local)
| (Placement::Anywhere, _)
| (Placement::CiOnly, RunnerEnv::Ci) => GateOutcome::Allow { warning: None },
(Placement::LocalOnly, RunnerEnv::Ci) => GateOutcome::Allow {
warning: Some(
"recipe placement = local-only 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 placement to \
`anywhere`."
.to_string(),
),
},
(Placement::CiOnly, RunnerEnv::Local) => {
if force {
GateOutcome::Allow {
warning: Some(
"recipe placement = ci-only 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 placement = ci-only 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: Placement, 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: Placement, 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: Placement, 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_local_only_on_local_allows_silently() {
assert_allowed(Placement::LocalOnly, RunnerEnv::Local, false);
}
#[test]
fn matrix_anywhere_on_local_allows_silently() {
assert_allowed(Placement::Anywhere, RunnerEnv::Local, false);
}
#[test]
fn matrix_anywhere_on_ci_allows_silently() {
assert_allowed(Placement::Anywhere, RunnerEnv::Ci, false);
}
#[test]
fn matrix_ci_only_on_ci_allows_silently() {
assert_allowed(Placement::CiOnly, RunnerEnv::Ci, false);
}
#[test]
fn matrix_local_only_on_ci_warns_but_allows() {
assert_allowed_with_warning(Placement::LocalOnly, RunnerEnv::Ci, false);
}
#[test]
fn matrix_ci_only_on_local_refuses_without_force() {
assert_refused(Placement::CiOnly, RunnerEnv::Local, false);
}
#[test]
fn matrix_ci_only_on_local_with_force_allows_with_warning() {
assert_allowed_with_warning(Placement::CiOnly, RunnerEnv::Local, true);
}
#[test]
fn force_is_no_op_on_already_allowed_cells() {
for p in [Placement::LocalOnly, Placement::Anywhere, Placement::CiOnly] {
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") };
}
}