mirror of
https://seed.radicle.dev/rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE.git
synced 2026-10-18 19:10:37 +08:00
261 lines
8.4 KiB
Rust
261 lines
8.4 KiB
Rust
use std::{
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
time::SystemTime,
|
|
};
|
|
|
|
use radicle_ci_broker::{
|
|
ergo::Oid,
|
|
msg::{helper::MessageHelperError, RepoId, Request, RunId},
|
|
};
|
|
|
|
use crate::{
|
|
runlog::{RunLog, RunLogError},
|
|
runspec::{RunSpec, RunSpecError},
|
|
};
|
|
|
|
// Exit code to indicate we didn't get one from the process.
|
|
const NO_EXIT: i32 = 999;
|
|
|
|
// Path to the repository's CI run specification. This is relative to
|
|
// the root of the repository.
|
|
pub const RUNSPEC_PATH: &str = ".radicle/native.yaml";
|
|
|
|
/// Execute the project-specific parts of a CI run. This needs to be
|
|
/// set up with `set_*` methods, and then the [`run`](Run::run) method
|
|
/// needs to be called.
|
|
#[derive(Debug)]
|
|
pub struct Run {
|
|
run_log: RunLog,
|
|
rid: Option<RepoId>,
|
|
request: Option<Request>,
|
|
repo_name: Option<String>,
|
|
commit: Option<Oid>,
|
|
storage: Option<PathBuf>,
|
|
src: PathBuf,
|
|
}
|
|
|
|
impl Run {
|
|
/// Create a new `Run`.
|
|
pub fn new(run_id: RunId, run_dir: &Path, run_log_filename: &Path) -> Result<Self, RunError> {
|
|
let mut run_log = RunLog::new(run_log_filename);
|
|
run_log.adapter_run_id(run_id);
|
|
|
|
Ok(Self {
|
|
run_log,
|
|
rid: None,
|
|
repo_name: None,
|
|
request: None,
|
|
commit: None,
|
|
storage: None,
|
|
src: run_dir.join("src"),
|
|
})
|
|
}
|
|
|
|
/// Set the message that triggered this run.
|
|
pub fn set_request(&mut self, req: Request) {
|
|
self.request = Some(req);
|
|
}
|
|
|
|
/// Set the git repository to use for this run.
|
|
pub fn set_repository(&mut self, rid: RepoId, repo_name: &str) {
|
|
self.rid = Some(rid);
|
|
self.repo_name = Some(repo_name.into());
|
|
}
|
|
|
|
/// Set the commit to use for this run.
|
|
pub fn set_commit(&mut self, commit: Oid) {
|
|
self.commit = Some(commit);
|
|
}
|
|
|
|
/// Set the location of the Radicle node git storage.
|
|
pub fn set_storage(&mut self, path: &Path) {
|
|
self.storage = Some(path.into());
|
|
}
|
|
|
|
/// Run CI on a project.
|
|
///
|
|
/// This runs CI for the project, and then clean up: persist the
|
|
/// per-run log, and clean up some disk space after a CI run. On
|
|
/// success, return the run log. A CI run has succeeded, if all
|
|
/// the commands run as part of it succeeded.
|
|
///
|
|
/// Note that this consume the `Run`.
|
|
pub fn run(mut self) -> Result<RunLog, RunError> {
|
|
// Execute the actual CI run.
|
|
let result = self.run_helper();
|
|
|
|
// If writing the run log fails, it will obscure any previous
|
|
// problem from the CI run. That's intentional: we want to
|
|
// make the node admin aware of the log writing problem, as
|
|
// that can be a problem for all future runs, e.g., due to a
|
|
// full file system, whereas, e.g., a syntax error in the code
|
|
// under test, is more fleeting.
|
|
let write_result = self.run_log.write();
|
|
if result.is_ok() {
|
|
write_result?;
|
|
}
|
|
|
|
// Likewise, if we can't clean up disk space, the node admin
|
|
// needs to know about that.
|
|
let rmdir_result = std::fs::remove_dir_all(&self.src)
|
|
.map_err(|e| RunError::RemoveDir(self.src.clone(), e));
|
|
if result.is_ok() {
|
|
rmdir_result?;
|
|
}
|
|
|
|
// Return result from the actual CI run.
|
|
result.map(|_| self.run_log)
|
|
}
|
|
|
|
// Execute CI run once, without worrying about cleanup. Store any
|
|
// problems in the run log. The caller is responsible for
|
|
// persisting the run log. This returns an error only if it's a
|
|
// programming error.
|
|
fn run_helper(&mut self) -> Result<(), RunError> {
|
|
// Get values fields we'll need to use, if they've been set.
|
|
let rid = self.rid.ok_or(RunError::Missing("rid"))?;
|
|
let repo_name = self
|
|
.repo_name
|
|
.as_ref()
|
|
.ok_or(RunError::Missing("repo_name"))?;
|
|
let request = self.request.clone().ok_or(RunError::Missing("request"))?;
|
|
let commit = self.commit.ok_or(RunError::Missing("commit"))?;
|
|
let storage = self.storage.as_ref().ok_or(RunError::Missing("storage"))?;
|
|
|
|
// Record metadata in the run log.
|
|
self.run_log.title("Log from Radicle native CI");
|
|
self.run_log.rid(rid, repo_name);
|
|
self.run_log.commit(commit);
|
|
self.run_log.request(request);
|
|
|
|
// Clone the repository and check out the right commit. If
|
|
// these fail, the problem is stored in the run log.
|
|
let repo_path = storage.join(rid.canonical());
|
|
let src = self.src.to_path_buf();
|
|
self.git_clone(&repo_path, &src)?;
|
|
self.git_checkout(commit, &src)?;
|
|
self.git_show(commit, &src)?;
|
|
|
|
let runspec_path = self.src.join(RUNSPEC_PATH);
|
|
let runspec = match RunSpec::from_file(&runspec_path) {
|
|
Ok(runspec) => {
|
|
self.run_log.runspec(runspec.clone());
|
|
runspec
|
|
}
|
|
Err(e) => {
|
|
// Log error in run log, then return. We can't do
|
|
// anything more if we don't have the run spec.
|
|
// However, return `Ok`, so that the CI engine doesn't
|
|
// report that the engine failed.
|
|
self.run_log.runspec_error(&e);
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let snippet = format!("set -xeuo pipefail\n{}", runspec.shell);
|
|
self.runcmd(&["bash", "-c", &snippet], &src)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn git_clone(&mut self, repo_path: &Path, src: &Path) -> Result<(), RunError> {
|
|
self.runcmd(
|
|
&[
|
|
"git",
|
|
"clone",
|
|
repo_path.to_str().unwrap(),
|
|
src.to_str().unwrap(),
|
|
],
|
|
Path::new("."),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn git_checkout(&mut self, commit: Oid, src: &Path) -> Result<(), RunError> {
|
|
self.runcmd(&["git", "config", "advice.detachedHead", "false"], src)?;
|
|
self.runcmd(&["git", "checkout", &commit.to_string()], src)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn git_show(&mut self, commit: Oid, src: &Path) -> Result<(), RunError> {
|
|
self.runcmd(&["git", "show", &commit.to_string()], src)?;
|
|
Ok(())
|
|
}
|
|
|
|
// Run an external command in a directory. Log the command and the
|
|
// result of running it to the run log. Return an error if the
|
|
// command could not be executed at all, but the exit code if it
|
|
// ran but failed.
|
|
fn runcmd(&mut self, argv: &[&str], cwd: &Path) -> Result<i32, RunError> {
|
|
if argv.is_empty() {
|
|
return Err(RunError::EmptyArgv);
|
|
}
|
|
|
|
let started = SystemTime::now();
|
|
let output = Command::new("bash")
|
|
.arg("-c")
|
|
.arg(r#""$@" 2>&1"#)
|
|
.arg("--")
|
|
.args(argv)
|
|
.current_dir(cwd)
|
|
.output()
|
|
.map_err(|e| RunError::Command(argv.iter().map(|s| s.to_string()).collect(), e))?;
|
|
let ended = SystemTime::now();
|
|
|
|
let exit = output.status.code().unwrap_or(NO_EXIT);
|
|
self.run_log.runcmd(
|
|
argv,
|
|
&cwd.canonicalize()
|
|
.map_err(|e| RunError::Canonicalize(cwd.into(), e))?,
|
|
exit,
|
|
&output.stdout,
|
|
started,
|
|
ended,
|
|
);
|
|
Ok(exit)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum RunError {
|
|
#[error("failed to create per-run parent directory {0}")]
|
|
CreateState(PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("failed to create per-run directory {0}")]
|
|
CreateRunDir(PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("failed to load Radicle profile")]
|
|
LoadProfile(#[source] radicle::profile::Error),
|
|
|
|
#[error("failed to remove {0}")]
|
|
RemoveDir(PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("programming error: failed to set field {0}")]
|
|
Unset(&'static str),
|
|
|
|
#[error(transparent)]
|
|
Message(#[from] MessageHelperError),
|
|
|
|
#[error(transparent)]
|
|
RunLog(#[from] RunLogError),
|
|
|
|
#[error(transparent)]
|
|
RunSpec(#[from] RunSpecError),
|
|
|
|
#[error("failed to run command {0:?}")]
|
|
Command(Vec<String>, #[source] std::io::Error),
|
|
|
|
#[error("command failed with exit code {0}: {1:?}")]
|
|
CommandFailed(i32, Vec<String>),
|
|
|
|
#[error("failed to make pathname absolute: {0}")]
|
|
Canonicalize(PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("programming error: function runcmd called with empty argv")]
|
|
EmptyArgv,
|
|
|
|
#[error("programming error: field '{0}' was not set in struct Run")]
|
|
Missing(&'static str),
|
|
}
|