1use std::{collections::BTreeMap, path::Path, time::Instant};
5
6use ci_config::{Check, CiConfig, Trigger};
7use crypto::{Conclusion, FailureClass};
8
9use crate::{
10 cache::{prepare_caches, restore_worktree_cache_dirs, save_caches},
11 classify::{Disposition, classify},
12 env::HermeticEnv,
13 model::{AttemptRecord, CheckResult, ExecutionContext, RunControls, RunOptions},
14 proc_group::ProcGroupRegistry,
15 process::{RunOutput, run_process},
16 result::{CompletedRun, finalize, infra_result, skipped_result},
17 result_cache::{ResultCache, ResultCacheError, SpotCheck, with_cache},
18};
19
20pub fn run_checks(
22 config: &CiConfig,
23 context: &ExecutionContext,
24 options: &RunOptions<'_>,
25) -> Result<Vec<CheckResult>, ResultCacheError> {
26 run_checks_with(config, context, options, &RunControls::default())
27}
28
29pub fn run_checks_with(
31 config: &CiConfig,
32 context: &ExecutionContext,
33 options: &RunOptions<'_>,
34 controls: &RunControls<'_>,
35) -> Result<Vec<CheckResult>, ResultCacheError> {
36 let default_environment = HermeticEnv::new();
37 let environment = controls.hermetic_env.unwrap_or(&default_environment);
38 let default_cache_root = options.workdir.join(".hci-cache");
39 let cache_root = controls.cache_root.unwrap_or(&default_cache_root);
40 let resolved = ResolvedRun {
41 options,
42 environment,
43 cache_root,
44 proc_groups: controls.proc_groups.as_ref(),
45 result_cache: controls.result_cache,
46 spot_check: controls.spot_check,
47 };
48 let results = config
49 .checks
50 .iter()
51 .map(|check| match &controls.trigger {
52 Some(trigger) if !check_runs_for_trigger(&check.triggers, trigger) => {
53 Ok(skipped_result(check, context, &resolved))
54 }
55 _ => run_one_check(check, context, &resolved),
56 })
57 .collect::<Result<Vec<_>, _>>()?;
58 restore_worktree_cache_dirs(options.workdir, &config.checks);
59 Ok(results)
60}
61
62pub(crate) struct ResolvedRun<'a> {
63 pub(crate) options: &'a RunOptions<'a>,
64 pub(crate) environment: &'a HermeticEnv,
65 pub(crate) cache_root: &'a Path,
66 pub(crate) proc_groups: Option<&'a ProcGroupRegistry>,
67 pub(crate) result_cache: Option<&'a dyn ResultCache>,
68 pub(crate) spot_check: SpotCheck,
69}
70
71fn run_one_check(
72 check: &Check,
73 context: &ExecutionContext,
74 run: &ResolvedRun<'_>,
75) -> Result<CheckResult, ResultCacheError> {
76 let service_environment = declared_service_env(check);
77 let key_environment = run
78 .environment
79 .build(&check.env, &service_environment, &BTreeMap::new());
80 with_cache(check, context, run, &key_environment, || {
81 run_one_check_uncached(check, context, run, &service_environment)
82 })
83}
84
85fn declared_service_env(check: &Check) -> BTreeMap<String, String> {
86 check
87 .services
88 .iter()
89 .flat_map(|service| {
90 service
91 .env
92 .iter()
93 .map(|entry| (entry.0.clone(), entry.1.clone()))
94 })
95 .collect()
96}
97
98fn run_one_check_uncached(
99 check: &Check,
100 context: &ExecutionContext,
101 run: &ResolvedRun<'_>,
102 service_environment: &BTreeMap<String, String>,
103) -> CheckResult {
104 let started_at = (run.options.now_rfc3339)();
105 let started = Instant::now();
106 let caches = match prepare_caches(
107 &check.name,
108 &check.cache_paths,
109 run.options.workdir,
110 run.cache_root,
111 ) {
112 Ok(caches) => caches,
113 Err(error) => {
114 return infra_result(
115 check,
116 context,
117 run,
118 started_at,
119 started.elapsed(),
120 "cache_paths",
121 &error.to_string(),
122 );
123 }
124 };
125 let services = match run.options.services.up(&check.services) {
126 Ok(services) => services,
127 Err(error) => {
128 return infra_result(
129 check,
130 context,
131 run,
132 started_at,
133 started.elapsed(),
134 "service_provisioning",
135 &format!("service provisioning failed: {error}"),
136 );
137 }
138 };
139 let environment = run
140 .environment
141 .build(&check.env, service_environment, &caches.env);
142 let (last, attempts) = run_attempts(check, run, &environment);
143 let _ = run.options.services.down(services);
144 if let Err(error) = save_caches(&caches) {
145 return infra_result(
146 check,
147 context,
148 run,
149 started_at,
150 started.elapsed(),
151 "cache_save",
152 &error.to_string(),
153 );
154 }
155 finalize(
156 check,
157 context,
158 CompletedRun {
159 output: last,
160 attempts,
161 environment,
162 started_at,
163 finished_at: (run.options.now_rfc3339)(),
164 duration: started.elapsed(),
165 },
166 )
167}
168
169fn run_attempts(
170 check: &Check,
171 run: &ResolvedRun<'_>,
172 environment: &BTreeMap<String, String>,
173) -> (RunOutput, Vec<AttemptRecord>) {
174 let mut last = RunOutput::default();
175 let mut records = Vec::new();
176 for attempt in 1..=check.retry.max.saturating_add(1) {
177 let started = Instant::now();
178 last = run_process(check, run.options.workdir, environment, run.proc_groups);
179 let flake = last.disposition != Disposition::Success
180 && matches_flake_signature(check, &last.combined_output);
181 records.push(AttemptRecord {
182 attempt,
183 conclusion: disposition_conclusion(last.disposition, &last.combined_output),
184 duration_ms: u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
185 flake_matched: flake,
186 });
187 if last.disposition == Disposition::Success || !flake {
188 break;
189 }
190 }
191 (last, records)
192}
193
194fn matches_flake_signature(check: &Check, output: &str) -> bool {
195 check.retry.flake_signatures.iter().any(|pattern| {
196 regex::Regex::new(pattern)
197 .map(|regex| regex.is_match(output))
198 .unwrap_or(false)
199 })
200}
201
202fn disposition_conclusion(disposition: Disposition, output: &str) -> Conclusion {
203 match classify(disposition, output) {
204 None => Conclusion::Success,
205 Some(FailureClass::Timeout) => Conclusion::TimedOut,
206 Some(FailureClass::Infra) => Conclusion::InfraError,
207 Some(_) => Conclusion::Failure,
208 }
209}
210
211fn check_runs_for_trigger(check: &[Trigger], pick: &Trigger) -> bool {
212 if check.is_empty() {
213 return matches!(pick, Trigger::Push);
214 }
215 check.iter().any(|trigger| {
216 matches!(
217 (trigger, pick),
218 (Trigger::Push, Trigger::Push)
219 | (Trigger::Manual, Trigger::Manual)
220 | (Trigger::Cron(_), Trigger::Cron(_))
221 )
222 })
223}