1use std::path::PathBuf;
2
3use uuid::Uuid;
4
5use radicle::prelude::Profile;
6use radicle_ci_broker::{
7 ergo::Oid,
8 msg::{
9 helper::{
10 read_request, write_failed, write_succeeded, write_triggered, MessageHelperError,
11 },
12 EventCommonFields, Patch, PatchEvent, PushEvent, RepoId, Repository, Request, RunId,
13 RunResult,
14 },
15};
16
17use crate::{
18 config::{Config, ConfigError},
19 logfile::{AdminLog, LogError},
20 run::{Run, RunError},
21 runlog::RunLogError,
22 runspec::RunSpecError,
23};
24
25#[derive(Debug)]
27pub struct Engine {
28 config: Config,
29 adminlog: AdminLog,
30 result: Option<RunResult>,
31}
32
33impl Engine {
34 #[allow(clippy::result_large_err)]
40 pub fn new() -> Result<Self, EngineError> {
41 let config = Config::load_via_env()?;
45 let adminlog = config.open_log()?;
46
47 Ok(Self {
50 config,
51 adminlog,
52 result: None,
53 })
54 }
55
56 pub fn config(&self) -> &Config {
58 &self.config
59 }
60
61 #[allow(clippy::result_large_err)]
66 pub fn run(&mut self) -> Result<bool, EngineError> {
67 let req = match self.setup() {
68 Ok(req) => req,
69 Err(e) => {
70 self.adminlog.writeln(&format!("Error setting up: {e}"))?;
71 return Err(e);
72 }
73 };
74
75 let mut success = false;
77 match &req {
78 Request::Trigger {
79 common:
80 EventCommonFields {
81 repository: Repository { name, .. },
82 ..
83 },
84 push: Some(PushEvent { branch, .. }),
85 ..
86 } => {
87 let repo = req.repo();
88 let commit = req.commit().map_err(EngineError::BrokerMessage)?;
89
90 match self.run_helper(repo, name, req.clone(), commit, Some(branch), None) {
91 Ok(true) => success = true,
92 Ok(false) => (),
93 Err(e) => {
94 self.adminlog.writeln(&format!("Error running CI: {e}"))?;
99 return Err(e);
100 }
101 }
102 }
103 Request::Trigger {
104 common:
105 EventCommonFields {
106 repository: Repository { name, .. },
107 ..
108 },
109 patch:
110 Some(PatchEvent {
111 patch: Patch { id, title, .. },
112 ..
113 }),
114 ..
115 } => {
116 let repo = req.repo();
117 let commit = req.commit().map_err(EngineError::BrokerMessage)?;
118 self.adminlog
119 .writeln(&format!("run CI for {repo} commit {commit}"))?;
120
121 match self.run_helper(repo, name, req.clone(), commit, None, Some((*id, title))) {
122 Ok(true) => success = true,
123 Ok(false) => (),
124 Err(e) => {
125 self.adminlog.writeln(&format!("Error running CI: {e}"))?;
130 return Err(e);
131 }
132 }
133 }
134 _ => {
135 self.adminlog
137 .writeln("First request was not a message to trigger a run.")?;
138 return Err(EngineError::NotTrigger(req));
139 }
140 }
141
142 if let Err(e) = self.finish() {
143 self.adminlog.writeln(&format!("Error finishing up: {e}"))?;
144 return Err(e);
145 }
146
147 Ok(success)
148 }
149
150 #[allow(clippy::result_large_err)]
153 fn setup(&mut self) -> Result<Request, EngineError> {
154 self.adminlog.writeln("Native CI run starts")?;
156
157 let req = read_request()?;
159 self.adminlog.writeln(&format!("request: {req:#?}"))?;
160
161 Ok(req)
162 }
163
164 #[allow(clippy::result_large_err)]
167 fn finish(&mut self) -> Result<(), EngineError> {
168 match &self.result {
170 Some(RunResult::Success) => write_succeeded()?,
171 Some(RunResult::Failure) => write_failed()?,
172 _ => panic!("do not know how to handle {:#?}", self.result),
173 }
174
175 self.adminlog.writeln("Native CI ends successfully")?;
177
178 Ok(())
179 }
180
181 #[allow(clippy::result_large_err)]
184 fn run_helper(
185 &mut self,
186 rid: RepoId,
187 name: &str,
188 req: Request,
189 commit: Oid,
190 branch: Option<&str>,
191 patch: Option<(Oid, &str)>,
192 ) -> Result<bool, EngineError> {
193 let (run_id, run_dir) = mkdir_run(&self.config)?;
196 let run_id = RunId::from(format!("{run_id}").as_str());
197
198 let run_log_filename = run_dir.join("log.html");
201
202 let profile = Profile::load().map_err(EngineError::LoadProfile)?;
206 let storage = profile.storage.path();
207
208 if let Some(url) = &self.config.base_url {
210 let url = if url.ends_with('/') {
211 format!("{url}{run_id}/log.html")
212 } else {
213 format!("{url}/{run_id}/log.html")
214 };
215 write_triggered(&run_id, Some(&url))?;
216 } else {
217 write_triggered(&run_id, None)?;
218 }
219
220 let mut run = Run::new(run_id, &run_dir, &run_log_filename)?;
222 run.set_repository(rid, name);
223 run.set_request(req);
224 run.set_commit(commit);
225 run.set_storage(storage);
226
227 let result = run.run();
230 if let Ok(mut run_log) = result {
231 if let Some(branch) = branch {
232 run_log.branch(branch);
233 }
234 if let Some((patch, title)) = patch {
235 run_log.patch(patch, title);
236 }
237 self.result = if run_log.all_commands_succeeded() {
238 Some(RunResult::Success)
239 } else {
240 Some(RunResult::Failure)
241 };
242 let all = run_log.all_commands_succeeded();
243 Ok(all)
244 } else {
245 self.result = Some(RunResult::Failure);
246 Ok(false)
247 }
248 }
249
250 #[allow(clippy::result_large_err)]
253 pub fn report(&mut self) -> Result<(), EngineError> {
254 Ok(())
255 }
256}
257
258#[allow(clippy::result_large_err)]
260fn mkdir_run(config: &Config) -> Result<(Uuid, PathBuf), EngineError> {
261 let state = &config.state;
262 if !state.exists() {
263 std::fs::create_dir_all(state).map_err(|e| EngineError::CreateState(state.into(), e))?;
264 }
265
266 let run_id = Uuid::new_v4();
267 let run_dir = state.join(run_id.to_string());
268 std::fs::create_dir(&run_dir).map_err(|e| EngineError::CreateRunDir(run_dir.clone(), e))?;
269 Ok((run_id, run_dir))
270}
271
272#[derive(Debug, thiserror::Error)]
273#[allow(clippy::large_enum_variant)]
274pub enum EngineError {
275 #[error("failed to create per-run parent directory {0}")]
276 CreateState(PathBuf, #[source] std::io::Error),
277
278 #[error("failed to create per-run directory {0}")]
279 CreateRunDir(PathBuf, #[source] std::io::Error),
280
281 #[error("failed to load Radicle profile")]
282 LoadProfile(#[source] radicle::profile::Error),
283
284 #[error("programming error: failed to set field {0}")]
285 Unset(&'static str),
286
287 #[error(transparent)]
288 Config(#[from] ConfigError),
289
290 #[error(transparent)]
291 Log(#[from] LogError),
292
293 #[error(transparent)]
294 BrokerMessage(#[from] radicle_ci_broker::msg::MessageError),
295
296 #[error(transparent)]
297 Message(#[from] MessageHelperError),
298
299 #[error(transparent)]
300 RunLog(#[from] RunLogError),
301
302 #[error(transparent)]
303 RunSpec(#[from] RunSpecError),
304
305 #[error(transparent)]
306 Run(#[from] RunError),
307
308 #[error("request message was not a trigger message: {0:?}")]
309 NotTrigger(Request),
310}