1use crate::types::Placement;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum RunnerEnv {
30 Local,
33 Ci,
35}
36
37impl RunnerEnv {
38 pub fn detect() -> Self {
41 if env_truthy("GITHUB_ACTIONS") || env_truthy("CI") {
42 RunnerEnv::Ci
43 } else {
44 RunnerEnv::Local
45 }
46 }
47}
48
49fn env_truthy(key: &str) -> bool {
50 matches!(
51 std::env::var(key).ok().as_deref(),
52 Some("1") | Some("true") | Some("TRUE") | Some("True"),
53 )
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum GateOutcome {
59 Allow { warning: Option<String> },
62 Refuse { reason: String },
64}
65
66pub fn evaluate(placement: Placement, env: RunnerEnv, force: bool) -> GateOutcome {
73 match (placement, env) {
74 (Placement::LocalOnly, RunnerEnv::Local)
76 | (Placement::Anywhere, _)
77 | (Placement::CiOnly, RunnerEnv::Ci) => GateOutcome::Allow { warning: None },
78
79 (Placement::LocalOnly, RunnerEnv::Ci) => GateOutcome::Allow {
84 warning: Some(
85 "recipe placement = local-only but runner is CI — the run's output \
86 (installed binary, files in the camp tree, …) is meaningless on a \
87 CI runner. Consider splitting the recipe or flipping placement to \
88 `anywhere`."
89 .to_string(),
90 ),
91 },
92
93 (Placement::CiOnly, RunnerEnv::Local) => {
96 if force {
97 GateOutcome::Allow {
98 warning: Some(
99 "recipe placement = ci-only but --force was passed; running \
100 locally. Steps that depend on CI secrets or signing identity \
101 will fail unless your environment already provides them."
102 .to_string(),
103 ),
104 }
105 } else {
106 GateOutcome::Refuse {
107 reason: "recipe placement = ci-only and this is not a CI runner — \
108 the recipe needs secrets, signing identity, or a clean \
109 runner that don't exist locally. Pass --force to run \
110 anyway, or run it from a CI workflow."
111 .to_string(),
112 }
113 }
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn assert_allowed(p: Placement, e: RunnerEnv, force: bool) {
123 match evaluate(p, e, force) {
124 GateOutcome::Allow { .. } => {}
125 other => panic!("expected Allow for ({p:?}, {e:?}, force={force}); got {other:?}"),
126 }
127 }
128
129 fn assert_allowed_with_warning(p: Placement, e: RunnerEnv, force: bool) {
130 match evaluate(p, e, force) {
131 GateOutcome::Allow { warning: Some(_) } => {}
132 other => panic!(
133 "expected Allow {{warning: Some(_)}} for ({p:?}, {e:?}, force={force}); got {other:?}"
134 ),
135 }
136 }
137
138 fn assert_refused(p: Placement, e: RunnerEnv, force: bool) {
139 match evaluate(p, e, force) {
140 GateOutcome::Refuse { .. } => {}
141 other => panic!("expected Refuse for ({p:?}, {e:?}, force={force}); got {other:?}"),
142 }
143 }
144
145 #[test]
148 fn matrix_local_only_on_local_allows_silently() {
149 assert_allowed(Placement::LocalOnly, RunnerEnv::Local, false);
150 }
151
152 #[test]
153 fn matrix_anywhere_on_local_allows_silently() {
154 assert_allowed(Placement::Anywhere, RunnerEnv::Local, false);
155 }
156
157 #[test]
158 fn matrix_anywhere_on_ci_allows_silently() {
159 assert_allowed(Placement::Anywhere, RunnerEnv::Ci, false);
160 }
161
162 #[test]
163 fn matrix_ci_only_on_ci_allows_silently() {
164 assert_allowed(Placement::CiOnly, RunnerEnv::Ci, false);
165 }
166
167 #[test]
168 fn matrix_local_only_on_ci_warns_but_allows() {
169 assert_allowed_with_warning(Placement::LocalOnly, RunnerEnv::Ci, false);
170 }
171
172 #[test]
173 fn matrix_ci_only_on_local_refuses_without_force() {
174 assert_refused(Placement::CiOnly, RunnerEnv::Local, false);
175 }
176
177 #[test]
178 fn matrix_ci_only_on_local_with_force_allows_with_warning() {
179 assert_allowed_with_warning(Placement::CiOnly, RunnerEnv::Local, true);
180 }
181
182 #[test]
185 fn force_is_no_op_on_already_allowed_cells() {
186 for p in [Placement::LocalOnly, Placement::Anywhere, Placement::CiOnly] {
187 for e in [RunnerEnv::Local, RunnerEnv::Ci] {
188 let without = evaluate(p, e, false);
189 if matches!(without, GateOutcome::Allow { warning: None }) {
190 let with_force = evaluate(p, e, true);
191 assert_eq!(
192 with_force, without,
193 "force should not change behavior for ({p:?}, {e:?})"
194 );
195 }
196 }
197 }
198 }
199
200 #[test]
203 fn env_truthy_recognizes_canonical_values() {
204 for val in ["1", "true", "TRUE", "True"] {
209 unsafe { std::env::set_var("__QED_GATE_TEST", val) };
210 assert!(env_truthy("__QED_GATE_TEST"), "truthy: {val}");
211 }
212 for val in ["0", "false", "no", ""] {
213 unsafe { std::env::set_var("__QED_GATE_TEST", val) };
214 assert!(!env_truthy("__QED_GATE_TEST"), "not truthy: {val}");
215 }
216 unsafe { std::env::remove_var("__QED_GATE_TEST") };
217 }
218}