1use std::{
2 path::{Path, PathBuf},
3 process::Command,
4 time::SystemTime,
5};
6
7use radicle_ci_broker::{
8 ergo::Oid,
9 msg::{helper::MessageHelperError, RepoId, Request, RunId},
10};
11
12use crate::{
13 runlog::{RunLog, RunLogError},
14 runspec::{RunSpec, RunSpecError},
15};
16
17const NO_EXIT: i32 = 999;
19
20pub const RUNSPEC_PATH: &str = ".radicle/native.yaml";
23
24#[derive(Debug)]
28pub struct Run {
29 run_log: RunLog,
30 rid: Option<RepoId>,
31 request: Option<Request>,
32 repo_name: Option<String>,
33 commit: Option<Oid>,
34 storage: Option<PathBuf>,
35 src: PathBuf,
36}
37
38impl Run {
39 pub fn new(run_id: RunId, run_dir: &Path, run_log_filename: &Path) -> Result<Self, RunError> {
41 let mut run_log = RunLog::new(run_log_filename);
42 run_log.adapter_run_id(run_id);
43
44 Ok(Self {
45 run_log,
46 rid: None,
47 repo_name: None,
48 request: None,
49 commit: None,
50 storage: None,
51 src: run_dir.join("src"),
52 })
53 }
54
55 pub fn set_request(&mut self, req: Request) {
57 self.request = Some(req);
58 }
59
60 pub fn set_repository(&mut self, rid: RepoId, repo_name: &str) {
62 self.rid = Some(rid);
63 self.repo_name = Some(repo_name.into());
64 }
65
66 pub fn set_commit(&mut self, commit: Oid) {
68 self.commit = Some(commit);
69 }
70
71 pub fn set_storage(&mut self, path: &Path) {
73 self.storage = Some(path.into());
74 }
75
76 pub fn run(mut self) -> Result<RunLog, RunError> {
85 let result = self.run_helper();
87
88 let write_result = self.run_log.write();
95 if result.is_ok() {
96 write_result?;
97 }
98
99 let rmdir_result = std::fs::remove_dir_all(&self.src)
102 .map_err(|e| RunError::RemoveDir(self.src.clone(), e));
103 if result.is_ok() {
104 rmdir_result?;
105 }
106
107 result.map(|_| self.run_log)
109 }
110
111 fn run_helper(&mut self) -> Result<(), RunError> {
116 let rid = self.rid.ok_or(RunError::Missing("rid"))?;
118 let repo_name = self
119 .repo_name
120 .as_ref()
121 .ok_or(RunError::Missing("repo_name"))?;
122 let request = self.request.clone().ok_or(RunError::Missing("request"))?;
123 let commit = self.commit.ok_or(RunError::Missing("commit"))?;
124 let storage = self.storage.as_ref().ok_or(RunError::Missing("storage"))?;
125
126 self.run_log.title("Log from Radicle native CI");
128 self.run_log.rid(rid, repo_name);
129 self.run_log.commit(commit);
130 self.run_log.request(request);
131
132 let repo_path = storage.join(rid.canonical());
135 let src = self.src.to_path_buf();
136 self.git_clone(&repo_path, &src)?;
137 self.git_checkout(commit, &src)?;
138 self.git_show(commit, &src)?;
139
140 let runspec_path = self.src.join(RUNSPEC_PATH);
141 let runspec = match RunSpec::from_file(&runspec_path) {
142 Ok(runspec) => {
143 self.run_log.runspec(runspec.clone());
144 runspec
145 }
146 Err(e) => {
147 self.run_log.runspec_error(&e);
152 return Ok(());
153 }
154 };
155
156 let snippet = format!("set -xeuo pipefail\n{}", &runspec.shell);
157 self.runcmd(&["bash", "-c", &snippet], &src)?;
158
159 Ok(())
160 }
161
162 fn git_clone(&mut self, repo_path: &Path, src: &Path) -> Result<(), RunError> {
163 self.runcmd(
164 &[
165 "git",
166 "clone",
167 repo_path.to_str().unwrap(),
168 src.to_str().unwrap(),
169 ],
170 Path::new("."),
171 )?;
172 Ok(())
173 }
174
175 fn git_checkout(&mut self, commit: Oid, src: &Path) -> Result<(), RunError> {
176 self.runcmd(&["git", "config", "advice.detachedHead", "false"], src)?;
177 self.runcmd(&["git", "checkout", &commit.to_string()], src)?;
178 Ok(())
179 }
180
181 fn git_show(&mut self, commit: Oid, src: &Path) -> Result<(), RunError> {
182 self.runcmd(&["git", "show", &commit.to_string()], src)?;
183 Ok(())
184 }
185
186 fn runcmd(&mut self, argv: &[&str], cwd: &Path) -> Result<i32, RunError> {
191 if argv.is_empty() {
192 return Err(RunError::EmptyArgv);
193 }
194
195 let started = SystemTime::now();
196 let output = Command::new("bash")
197 .arg("-c")
198 .arg(r#""$@" 2>&1"#)
199 .arg("--")
200 .args(argv)
201 .current_dir(cwd)
202 .output()
203 .map_err(|e| RunError::Command(argv.iter().map(|s| s.to_string()).collect(), e))?;
204 let ended = SystemTime::now();
205
206 let exit = output.status.code().unwrap_or(NO_EXIT);
207 self.run_log.runcmd(
208 argv,
209 &cwd.canonicalize()
210 .map_err(|e| RunError::Canonicalize(cwd.into(), e))?,
211 exit,
212 &output.stdout,
213 started,
214 ended,
215 );
216 Ok(exit)
217 }
218}
219
220#[derive(Debug, thiserror::Error)]
221pub enum RunError {
222 #[error("failed to create per-run parent directory {0}")]
223 CreateState(PathBuf, #[source] std::io::Error),
224
225 #[error("failed to create per-run directory {0}")]
226 CreateRunDir(PathBuf, #[source] std::io::Error),
227
228 #[error("failed to load Radicle profile")]
229 LoadProfile(#[source] radicle::profile::Error),
230
231 #[error("failed to remove {0}")]
232 RemoveDir(PathBuf, #[source] std::io::Error),
233
234 #[error("programming error: failed to set field {0}")]
235 Unset(&'static str),
236
237 #[error(transparent)]
238 Message(#[from] MessageHelperError),
239
240 #[error(transparent)]
241 RunLog(#[from] RunLogError),
242
243 #[error(transparent)]
244 RunSpec(#[from] RunSpecError),
245
246 #[error("failed to run command {0:?}")]
247 Command(Vec<String>, #[source] std::io::Error),
248
249 #[error("command failed with exit code {0}: {1:?}")]
250 CommandFailed(i32, Vec<String>),
251
252 #[error("failed to make pathname absolute: {0}")]
253 Canonicalize(PathBuf, #[source] std::io::Error),
254
255 #[error("programming error: function runcmd called with empty argv")]
256 EmptyArgv,
257
258 #[error("programming error: field '{0}' was not set in struct Run")]
259 Missing(&'static str),
260}