Radish alpha
r
Radicle Job Collaborative Object
Radicle
Git (anonymous pull)
Log in to clone via SSH
feat! rename binary to rad-job
Lars Wirzenius committed 10 months ago
commit 0e8b3823f9e146783fc8cc1de08fe5bd33bf4a87
parent 26084b0d2f03f2e3c9a42b769be758252607dabd
2 files changed +475 -475
added src/bin/rad-job.rs
@@ -0,0 +1,475 @@
+
//! A program to create and inspect job COBs and the automated
+
//! processing runs they record, for a repository.
+
//!
+
//! Run `radicle-job --help` to see how to use the program.
+

+
use std::{collections::BTreeSet, error::Error, time::Duration};
+

+
use clap::Parser;
+
use serde::Serialize;
+
use url::Url;
+
use uuid::Uuid;
+

+
use radicle::{
+
    git::Oid,
+
    node::{
+
        sync::{Announcer, AnnouncerConfig, ReplicationFactor},
+
        Handle, Node,
+
    },
+
    prelude::{NodeId, Profile, ReadStorage, RepoId},
+
    storage::git::Repository,
+
};
+
use radicle_job::*;
+

+
const TIMEOUT: Duration = Duration::from_millis(5000);
+

+
fn main() {
+
    if let Err(err) = fallible_main() {
+
        eprintln!("ERROR: {err}");
+
        let mut err = err.source();
+
        while let Some(underlying) = err {
+
            eprintln!("caused by: {underlying}");
+
            err = underlying.source();
+
        }
+
        std::process::exit(1);
+
    }
+
}
+

+
fn fallible_main() -> Result<(), JobError> {
+
    let args = Args::parse();
+
    match &args.cmd {
+
        Cmd::Failed(x) => x.run(&args)?,
+
        Cmd::Job(x) => x.run(&args)?,
+
        Cmd::Jobs(x) => x.run(&args)?,
+
        Cmd::Run(x) => x.run(&args)?,
+
        Cmd::ShowJob(x) => x.run(&args)?,
+
        Cmd::Succeeded(x) => x.run(&args)?,
+
    }
+
    Ok(())
+
}
+

+
/// Create, update, and query Radicle job COBs.
+
///
+
/// A job COB records records results of automated processing of a
+
/// repository for a given Git commit, by one or more nodes. For
+
/// example, CI runs would be recorded as job COBs.
+
///
+
/// This program is a utility for Radicle developers to experiment
+
/// with and investigate job COBs. It allows you to see what jobs have
+
/// jobs and runs have been recorded. Output is JSON, for developer
+
/// convenience.
+
#[derive(Parser)]
+
struct Args {
+
    /// Use this repository. Default is the current working directory.
+
    #[clap(short, long)]
+
    repository: Option<RepoId>,
+

+
    #[clap(subcommand)]
+
    cmd: Cmd,
+
}
+

+
impl Args {
+
    fn repository(&self, profile: &Profile) -> Result<Repository, JobError> {
+
        let repo_id = if let Some(repo_id) = self.repository {
+
            repo_id
+
        } else {
+
            let (_repo, repo_id) = radicle::rad::cwd().map_err(JobError::Cwd)?;
+
            repo_id
+
        };
+
        profile
+
            .storage
+
            .repository(repo_id)
+
            .map_err(JobError::OpenRepository)
+
    }
+
}
+

+
#[derive(Parser)]
+
enum Cmd {
+
    Job(JobCmd),
+
    Jobs(JobsCmd),
+
    Run(RunCmd),
+
    ShowJob(ShowJobCmd),
+
    Failed(FailedCmd),
+
    Succeeded(SucceededCmd),
+
}
+

+
/// List all job COBs for a repository.
+
#[derive(Parser)]
+
struct JobsCmd {
+
    /// Format output in a more human oriented way than JSON.
+
    #[clap(long)]
+
    pretty: bool,
+
}
+

+
impl JobsCmd {
+
    fn run(&self, args: &Args) -> Result<(), JobError> {
+
        let profile = profile()?;
+
        let repo = args.repository(&profile)?;
+

+
        let jobs = jobs(&repo)?;
+
        let jobslist = JobsList::new(&jobs)?;
+

+
        if self.pretty {
+
            println!("{}", jobslist.pretty());
+
        } else {
+
            println!(
+
                "{}",
+
                serde_json::to_string_pretty(&jobslist).map_err(JobError::ToJson)?
+
            );
+
        }
+

+
        Ok(())
+
    }
+
}
+

+
#[derive(Serialize)]
+
struct JobsList {
+
    count: usize,
+
    jobs: Vec<ShownJob>,
+
}
+

+
impl JobsList {
+
    fn new(jobs: &Jobs<Repository>) -> Result<Self, JobError> {
+
        let mut all_jobs = vec![];
+
        for item in jobs.all().map_err(JobError::AllJobs)? {
+
            let (job_id, job) = item.map_err(JobError::AllJobsJob)?;
+
            let job_id = JobId::from(job_id);
+
            all_jobs.push(ShownJob::new(job_id, &job));
+
        }
+

+
        all_jobs.sort_by_cached_key(|job| job.job_id);
+

+
        Ok(Self {
+
            count: jobs.counts().map_err(JobError::JobCount)?,
+
            jobs: all_jobs,
+
        })
+
    }
+

+
    fn pretty(&self) -> String {
+
        fn line(s: &mut String, line: String) {
+
            s.push_str(&line);
+
            s.push('\n');
+
        }
+

+
        let mut s = String::new();
+

+
        line(&mut s, format!("count: {}", self.count));
+
        for shown in self.jobs.iter() {
+
            line(&mut s, format!("job {}", shown.job_id));
+
            for run in shown.runs.iter() {
+
                line(&mut s, format!("  node {}", run.node_id));
+
                for run2 in run.runs.iter() {
+
                    line(&mut s, format!("    run {} {:?}", run2.run_id, run2.status));
+
                    line(&mut s, format!("      log  {}", run2.log));
+
                }
+
            }
+
        }
+

+
        s
+
    }
+
}
+

+
/// Create a job COB for a specific Git commit.
+
///
+
/// Write the job ID to the standard output. It is not usually needed,
+
/// as job COBs are found using the Git commit ID they refer to.
+
#[derive(Parser)]
+
struct JobCmd {
+
    /// Git object id for the commit.
+
    oid: Oid,
+
}
+

+
impl JobCmd {
+
    fn run(&self, args: &Args) -> Result<(), JobError> {
+
        let profile = profile()?;
+
        let repo = args.repository(&profile)?;
+
        let signer = profile.signer().map_err(JobError::Signer)?;
+

+
        let mut jobs = jobs(&repo)?;
+
        let job = jobs
+
            .create(self.oid, &signer)
+
            .map_err(JobError::CreateJob)?;
+
        announce(&profile, repo.id)?;
+
        println!("{}", job.id());
+

+
        Ok(())
+
    }
+
}
+

+
/// Show the job COB for a Git commit.
+
#[derive(Parser)]
+
struct ShowJobCmd {
+
    /// Git object for the commit.
+
    oid: Oid,
+
}
+

+
impl ShowJobCmd {
+
    fn run(&self, args: &Args) -> Result<(), JobError> {
+
        let profile = profile()?;
+
        let repo = args.repository(&profile)?;
+

+
        let jobs = jobs(&repo)?;
+
        let job_id = job_for_commit(&jobs, self.oid)?;
+
        let job = jobs
+
            .get(&job_id)
+
            .map_err(JobError::GetJob)?
+
            .ok_or(JobError::NoSuchJob(job_id))?;
+

+
        let shown = ShownJob::new(job_id, &job);
+
        println!(
+
            "{}",
+
            serde_json::to_string_pretty(&shown).map_err(JobError::ToJson)?
+
        );
+

+
        Ok(())
+
    }
+
}
+

+
#[derive(Serialize)]
+
struct ShownJob {
+
    job_id: JobId,
+
    oid: Oid,
+
    runs: Vec<ShownRuns>,
+
}
+

+
impl ShownJob {
+
    fn new(job_id: JobId, job: &Job) -> Self {
+
        let mut runs: Vec<_> = job
+
            .runs()
+
            .iter()
+
            .map(|(node_id, runs)| {
+
                let mut runs: Vec<ShownRun> = runs
+
                    .iter()
+
                    .map(|(run_id, run)| ShownRun {
+
                        run_id: *run_id,
+
                        status: *run.status(),
+
                        log: run.log().clone(),
+
                    })
+
                    .collect();
+
                runs.sort_by_cached_key(|run| run.run_id);
+
                ShownRuns {
+
                    node_id: *node_id,
+
                    runs,
+
                }
+
            })
+
            .collect();
+
        runs.sort_by_cached_key(|run| run.node_id);
+

+
        Self {
+
            job_id,
+
            oid: *job.oid(),
+
            runs,
+
        }
+
    }
+
}
+

+
#[derive(Serialize)]
+
struct ShownRuns {
+
    node_id: NodeId,
+
    runs: Vec<ShownRun>,
+
}
+

+
#[derive(Serialize)]
+
struct ShownRun {
+
    run_id: Uuid,
+
    status: Status,
+
    log: Url,
+
}
+

+
/// Record a new run for an existing job COB.
+
///
+
/// The new run is marked as started. See subcommands "succeeded" and
+
/// "failed" to change status.
+
///
+
/// Write the UUID of the new run to standard output. It is needed to
+
/// change mark run as finished.
+
#[derive(Parser)]
+
struct RunCmd {
+
    /// Git commit which the run is processing.
+
    oid: Oid,
+
    /// URL to information about the run, such a a run log.
+
    url: Url,
+
}
+

+
impl RunCmd {
+
    fn run(&self, args: &Args) -> Result<(), JobError> {
+
        let profile = profile()?;
+
        let repo = args.repository(&profile)?;
+
        let signer = profile.signer().map_err(JobError::Signer)?;
+

+
        let mut jobs = jobs(&repo)?;
+
        let job_id = job_for_commit(&jobs, self.oid)?;
+
        let uuid = Uuid::new_v4();
+
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
+
        job.run(uuid, self.url.clone(), &signer)
+
            .map_err(JobError::AddRun)?;
+
        announce(&profile, repo.id)?;
+
        println!("{uuid}");
+
        Ok(())
+
    }
+
}
+

+
/// Mark a run as having finished successfully.
+
#[derive(Parser)]
+
struct SucceededCmd {
+
    /// Git commit which the run is processing.
+
    oid: Oid,
+
    /// UUID for the run.
+
    run: Uuid,
+
}
+

+
impl SucceededCmd {
+
    fn run(&self, args: &Args) -> Result<(), JobError> {
+
        let profile = profile()?;
+
        let repo = args.repository(&profile)?;
+
        let signer = profile.signer().map_err(JobError::Signer)?;
+

+
        let mut jobs = jobs(&repo)?;
+
        let job_id = job_for_commit(&jobs, self.oid)?;
+
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
+
        job.finish(self.run, Reason::Succeeded, &signer)
+
            .map_err(JobError::Finish)?;
+
        announce(&profile, repo.id)?;
+

+
        Ok(())
+
    }
+
}
+

+
/// Mark a run as having finished in failure.
+
#[derive(Parser)]
+
struct FailedCmd {
+
    /// Git commit which the run is processing.
+
    oid: Oid,
+
    /// UUID for the run.
+
    run: Uuid,
+
}
+

+
impl FailedCmd {
+
    fn run(&self, args: &Args) -> Result<(), JobError> {
+
        let profile = profile()?;
+
        let repo = args.repository(&profile)?;
+
        let signer = profile.signer().map_err(JobError::Signer)?;
+

+
        let mut jobs = jobs(&repo)?;
+
        let job_id = job_for_commit(&jobs, self.oid)?;
+
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
+
        job.finish(self.run, Reason::Failed, &signer)
+
            .map_err(JobError::Finish)?;
+
        announce(&profile, repo.id)?;
+

+
        Ok(())
+
    }
+
}
+

+
fn profile() -> Result<Profile, JobError> {
+
    Profile::load().map_err(JobError::Profile)
+
}
+

+
fn jobs<'a>(repo: &'a Repository) -> Result<Jobs<'a, Repository>, JobError> {
+
    Jobs::open(repo).map_err(JobError::Jobs)
+
}
+

+
fn job_for_commit<'a>(jobs: &Jobs<'a, Repository>, wanted: Oid) -> Result<JobId, JobError> {
+
    eprintln!("job_for_commit: wanted={wanted}");
+
    for item in jobs.all().map_err(JobError::AllJobs)? {
+
        let (job_id, job) = item.map_err(JobError::AllJobsJob)?;
+
        let job_id = JobId::from(job_id);
+
        eprintln!("job_for_commit: consider {job_id} with oid {}", job.oid());
+
        if job.oid() == &wanted {
+
            eprintln!("job_for_commit: wanted={wanted} => {job_id}");
+
            return Ok(job_id);
+
        }
+
    }
+

+
    Err(JobError::NoJob(wanted))
+
}
+

+
fn announce(profile: &Profile, repo_id: RepoId) -> Result<(), JobError> {
+
    let mut node = Node::new(profile.home.socket());
+

+
    let (synced, unsynced) = node.seeds(repo_id).map_err(JobError::Seeds)?.iter().fold(
+
        (BTreeSet::new(), BTreeSet::new()),
+
        |(mut synced, mut unsynced), seed| {
+
            if seed.is_synced() {
+
                synced.insert(seed.nid);
+
            } else {
+
                unsynced.insert(seed.nid);
+
            }
+
            (synced, unsynced)
+
        },
+
    );
+

+
    let announcer = Announcer::new(AnnouncerConfig::public(
+
        *profile.id(),
+
        ReplicationFactor::MustReach(1),
+
        BTreeSet::new(),
+
        synced,
+
        unsynced,
+
    ))
+
    .map_err(|_| JobError::Announcer)?;
+

+
    node.announce(repo_id, TIMEOUT, announcer, |_, _| ())
+
        .map_err(JobError::Announce)?;
+

+
    Ok(())
+
}
+

+
#[derive(Debug, thiserror::Error)]
+
enum JobError {
+
    #[error("failed to open repository in current directory")]
+
    Cwd(#[source] radicle::rad::RemoteError),
+

+
    #[error("failed to load Radicle profile")]
+
    Profile(#[source] radicle::profile::Error),
+

+
    #[error("failed to open repository in Radicle node storage")]
+
    OpenRepository(#[source] radicle::storage::RepositoryError),
+

+
    #[error("failed to list job COBs in repository")]
+
    Jobs(#[source] radicle::storage::RepositoryError),
+

+
    #[error("failed to get job COB count")]
+
    JobCount(#[source] radicle::cob::store::Error),
+

+
    #[error("failed to get all job COBs")]
+
    AllJobs(#[source] radicle::cob::store::Error),
+

+
    #[error("failed to create a new job COB")]
+
    CreateJob(#[source] radicle::cob::store::Error),
+

+
    #[error("failed to get a job COB")]
+
    GetJob(#[source] radicle::cob::store::Error),
+

+
    #[error("failed to create a signer for Radicle repository")]
+
    Signer(#[source] radicle::profile::Error),
+

+
    #[error("didn't find job COB {0}")]
+
    NoSuchJob(JobId),
+

+
    #[error("couldn't get job when iterating")]
+
    AllJobsJob(#[source] radicle::cob::store::Error),
+

+
    #[error("failed to get mutable job COB")]
+
    GetJobMut(#[source] radicle::cob::store::Error),
+

+
    #[error("failed to add a run to a job COB")]
+
    AddRun(#[source] radicle::cob::store::Error),
+

+
    #[error("could not mark a run as finished")]
+
    Finish(#[source] radicle::cob::store::Error),
+

+
    #[error("can't serialize to JSON")]
+
    ToJson(#[source] serde_json::Error),
+

+
    #[error("failed to find job COB for oid {0}")]
+
    NoJob(Oid),
+

+
    #[error("failed to announce COB change")]
+
    Announce(#[source] radicle::node::Error),
+

+
    #[error("failed to get seeds for node")]
+
    Seeds(#[source] radicle::node::Error),
+

+
    #[error("failed to create a COB announcer")]
+
    Announcer,
+
}
deleted src/main.rs
@@ -1,475 +0,0 @@
-
//! A program to create and inspect job COBs and the automated
-
//! processing runs they record, for a repository.
-
//!
-
//! Run `radicle-job --help` to see how to use the program.
-

-
use std::{collections::BTreeSet, error::Error, time::Duration};
-

-
use clap::Parser;
-
use serde::Serialize;
-
use url::Url;
-
use uuid::Uuid;
-

-
use radicle::{
-
    git::Oid,
-
    node::{
-
        sync::{Announcer, AnnouncerConfig, ReplicationFactor},
-
        Handle, Node,
-
    },
-
    prelude::{NodeId, Profile, ReadStorage, RepoId},
-
    storage::git::Repository,
-
};
-
use radicle_job::*;
-

-
const TIMEOUT: Duration = Duration::from_millis(5000);
-

-
fn main() {
-
    if let Err(err) = fallible_main() {
-
        eprintln!("ERROR: {err}");
-
        let mut err = err.source();
-
        while let Some(underlying) = err {
-
            eprintln!("caused by: {underlying}");
-
            err = underlying.source();
-
        }
-
        std::process::exit(1);
-
    }
-
}
-

-
fn fallible_main() -> Result<(), JobError> {
-
    let args = Args::parse();
-
    match &args.cmd {
-
        Cmd::Failed(x) => x.run(&args)?,
-
        Cmd::Job(x) => x.run(&args)?,
-
        Cmd::Jobs(x) => x.run(&args)?,
-
        Cmd::Run(x) => x.run(&args)?,
-
        Cmd::ShowJob(x) => x.run(&args)?,
-
        Cmd::Succeeded(x) => x.run(&args)?,
-
    }
-
    Ok(())
-
}
-

-
/// Create, update, and query Radicle job COBs.
-
///
-
/// A job COB records records results of automated processing of a
-
/// repository for a given Git commit, by one or more nodes. For
-
/// example, CI runs would be recorded as job COBs.
-
///
-
/// This program is a utility for Radicle developers to experiment
-
/// with and investigate job COBs. It allows you to see what jobs have
-
/// jobs and runs have been recorded. Output is JSON, for developer
-
/// convenience.
-
#[derive(Parser)]
-
struct Args {
-
    /// Use this repository. Default is the current working directory.
-
    #[clap(short, long)]
-
    repository: Option<RepoId>,
-

-
    #[clap(subcommand)]
-
    cmd: Cmd,
-
}
-

-
impl Args {
-
    fn repository(&self, profile: &Profile) -> Result<Repository, JobError> {
-
        let repo_id = if let Some(repo_id) = self.repository {
-
            repo_id
-
        } else {
-
            let (_repo, repo_id) = radicle::rad::cwd().map_err(JobError::Cwd)?;
-
            repo_id
-
        };
-
        profile
-
            .storage
-
            .repository(repo_id)
-
            .map_err(JobError::OpenRepository)
-
    }
-
}
-

-
#[derive(Parser)]
-
enum Cmd {
-
    Job(JobCmd),
-
    Jobs(JobsCmd),
-
    Run(RunCmd),
-
    ShowJob(ShowJobCmd),
-
    Failed(FailedCmd),
-
    Succeeded(SucceededCmd),
-
}
-

-
/// List all job COBs for a repository.
-
#[derive(Parser)]
-
struct JobsCmd {
-
    /// Format output in a more human oriented way than JSON.
-
    #[clap(long)]
-
    pretty: bool,
-
}
-

-
impl JobsCmd {
-
    fn run(&self, args: &Args) -> Result<(), JobError> {
-
        let profile = profile()?;
-
        let repo = args.repository(&profile)?;
-

-
        let jobs = jobs(&repo)?;
-
        let jobslist = JobsList::new(&jobs)?;
-

-
        if self.pretty {
-
            println!("{}", jobslist.pretty());
-
        } else {
-
            println!(
-
                "{}",
-
                serde_json::to_string_pretty(&jobslist).map_err(JobError::ToJson)?
-
            );
-
        }
-

-
        Ok(())
-
    }
-
}
-

-
#[derive(Serialize)]
-
struct JobsList {
-
    count: usize,
-
    jobs: Vec<ShownJob>,
-
}
-

-
impl JobsList {
-
    fn new(jobs: &Jobs<Repository>) -> Result<Self, JobError> {
-
        let mut all_jobs = vec![];
-
        for item in jobs.all().map_err(JobError::AllJobs)? {
-
            let (job_id, job) = item.map_err(JobError::AllJobsJob)?;
-
            let job_id = JobId::from(job_id);
-
            all_jobs.push(ShownJob::new(job_id, &job));
-
        }
-

-
        all_jobs.sort_by_cached_key(|job| job.job_id);
-

-
        Ok(Self {
-
            count: jobs.counts().map_err(JobError::JobCount)?,
-
            jobs: all_jobs,
-
        })
-
    }
-

-
    fn pretty(&self) -> String {
-
        fn line(s: &mut String, line: String) {
-
            s.push_str(&line);
-
            s.push('\n');
-
        }
-

-
        let mut s = String::new();
-

-
        line(&mut s, format!("count: {}", self.count));
-
        for shown in self.jobs.iter() {
-
            line(&mut s, format!("job {}", shown.job_id));
-
            for run in shown.runs.iter() {
-
                line(&mut s, format!("  node {}", run.node_id));
-
                for run2 in run.runs.iter() {
-
                    line(&mut s, format!("    run {} {:?}", run2.run_id, run2.status));
-
                    line(&mut s, format!("      log  {}", run2.log));
-
                }
-
            }
-
        }
-

-
        s
-
    }
-
}
-

-
/// Create a job COB for a specific Git commit.
-
///
-
/// Write the job ID to the standard output. It is not usually needed,
-
/// as job COBs are found using the Git commit ID they refer to.
-
#[derive(Parser)]
-
struct JobCmd {
-
    /// Git object id for the commit.
-
    oid: Oid,
-
}
-

-
impl JobCmd {
-
    fn run(&self, args: &Args) -> Result<(), JobError> {
-
        let profile = profile()?;
-
        let repo = args.repository(&profile)?;
-
        let signer = profile.signer().map_err(JobError::Signer)?;
-

-
        let mut jobs = jobs(&repo)?;
-
        let job = jobs
-
            .create(self.oid, &signer)
-
            .map_err(JobError::CreateJob)?;
-
        announce(&profile, repo.id)?;
-
        println!("{}", job.id());
-

-
        Ok(())
-
    }
-
}
-

-
/// Show the job COB for a Git commit.
-
#[derive(Parser)]
-
struct ShowJobCmd {
-
    /// Git object for the commit.
-
    oid: Oid,
-
}
-

-
impl ShowJobCmd {
-
    fn run(&self, args: &Args) -> Result<(), JobError> {
-
        let profile = profile()?;
-
        let repo = args.repository(&profile)?;
-

-
        let jobs = jobs(&repo)?;
-
        let job_id = job_for_commit(&jobs, self.oid)?;
-
        let job = jobs
-
            .get(&job_id)
-
            .map_err(JobError::GetJob)?
-
            .ok_or(JobError::NoSuchJob(job_id))?;
-

-
        let shown = ShownJob::new(job_id, &job);
-
        println!(
-
            "{}",
-
            serde_json::to_string_pretty(&shown).map_err(JobError::ToJson)?
-
        );
-

-
        Ok(())
-
    }
-
}
-

-
#[derive(Serialize)]
-
struct ShownJob {
-
    job_id: JobId,
-
    oid: Oid,
-
    runs: Vec<ShownRuns>,
-
}
-

-
impl ShownJob {
-
    fn new(job_id: JobId, job: &Job) -> Self {
-
        let mut runs: Vec<_> = job
-
            .runs()
-
            .iter()
-
            .map(|(node_id, runs)| {
-
                let mut runs: Vec<ShownRun> = runs
-
                    .iter()
-
                    .map(|(run_id, run)| ShownRun {
-
                        run_id: *run_id,
-
                        status: *run.status(),
-
                        log: run.log().clone(),
-
                    })
-
                    .collect();
-
                runs.sort_by_cached_key(|run| run.run_id);
-
                ShownRuns {
-
                    node_id: *node_id,
-
                    runs,
-
                }
-
            })
-
            .collect();
-
        runs.sort_by_cached_key(|run| run.node_id);
-

-
        Self {
-
            job_id,
-
            oid: *job.oid(),
-
            runs,
-
        }
-
    }
-
}
-

-
#[derive(Serialize)]
-
struct ShownRuns {
-
    node_id: NodeId,
-
    runs: Vec<ShownRun>,
-
}
-

-
#[derive(Serialize)]
-
struct ShownRun {
-
    run_id: Uuid,
-
    status: Status,
-
    log: Url,
-
}
-

-
/// Record a new run for an existing job COB.
-
///
-
/// The new run is marked as started. See subcommands "succeeded" and
-
/// "failed" to change status.
-
///
-
/// Write the UUID of the new run to standard output. It is needed to
-
/// change mark run as finished.
-
#[derive(Parser)]
-
struct RunCmd {
-
    /// Git commit which the run is processing.
-
    oid: Oid,
-
    /// URL to information about the run, such a a run log.
-
    url: Url,
-
}
-

-
impl RunCmd {
-
    fn run(&self, args: &Args) -> Result<(), JobError> {
-
        let profile = profile()?;
-
        let repo = args.repository(&profile)?;
-
        let signer = profile.signer().map_err(JobError::Signer)?;
-

-
        let mut jobs = jobs(&repo)?;
-
        let job_id = job_for_commit(&jobs, self.oid)?;
-
        let uuid = Uuid::new_v4();
-
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
-
        job.run(uuid, self.url.clone(), &signer)
-
            .map_err(JobError::AddRun)?;
-
        announce(&profile, repo.id)?;
-
        println!("{uuid}");
-
        Ok(())
-
    }
-
}
-

-
/// Mark a run as having finished successfully.
-
#[derive(Parser)]
-
struct SucceededCmd {
-
    /// Git commit which the run is processing.
-
    oid: Oid,
-
    /// UUID for the run.
-
    run: Uuid,
-
}
-

-
impl SucceededCmd {
-
    fn run(&self, args: &Args) -> Result<(), JobError> {
-
        let profile = profile()?;
-
        let repo = args.repository(&profile)?;
-
        let signer = profile.signer().map_err(JobError::Signer)?;
-

-
        let mut jobs = jobs(&repo)?;
-
        let job_id = job_for_commit(&jobs, self.oid)?;
-
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
-
        job.finish(self.run, Reason::Succeeded, &signer)
-
            .map_err(JobError::Finish)?;
-
        announce(&profile, repo.id)?;
-

-
        Ok(())
-
    }
-
}
-

-
/// Mark a run as having finished in failure.
-
#[derive(Parser)]
-
struct FailedCmd {
-
    /// Git commit which the run is processing.
-
    oid: Oid,
-
    /// UUID for the run.
-
    run: Uuid,
-
}
-

-
impl FailedCmd {
-
    fn run(&self, args: &Args) -> Result<(), JobError> {
-
        let profile = profile()?;
-
        let repo = args.repository(&profile)?;
-
        let signer = profile.signer().map_err(JobError::Signer)?;
-

-
        let mut jobs = jobs(&repo)?;
-
        let job_id = job_for_commit(&jobs, self.oid)?;
-
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
-
        job.finish(self.run, Reason::Failed, &signer)
-
            .map_err(JobError::Finish)?;
-
        announce(&profile, repo.id)?;
-

-
        Ok(())
-
    }
-
}
-

-
fn profile() -> Result<Profile, JobError> {
-
    Profile::load().map_err(JobError::Profile)
-
}
-

-
fn jobs<'a>(repo: &'a Repository) -> Result<Jobs<'a, Repository>, JobError> {
-
    Jobs::open(repo).map_err(JobError::Jobs)
-
}
-

-
fn job_for_commit<'a>(jobs: &Jobs<'a, Repository>, wanted: Oid) -> Result<JobId, JobError> {
-
    eprintln!("job_for_commit: wanted={wanted}");
-
    for item in jobs.all().map_err(JobError::AllJobs)? {
-
        let (job_id, job) = item.map_err(JobError::AllJobsJob)?;
-
        let job_id = JobId::from(job_id);
-
        eprintln!("job_for_commit: consider {job_id} with oid {}", job.oid());
-
        if job.oid() == &wanted {
-
            eprintln!("job_for_commit: wanted={wanted} => {job_id}");
-
            return Ok(job_id);
-
        }
-
    }
-

-
    Err(JobError::NoJob(wanted))
-
}
-

-
fn announce(profile: &Profile, repo_id: RepoId) -> Result<(), JobError> {
-
    let mut node = Node::new(profile.home.socket());
-

-
    let (synced, unsynced) = node.seeds(repo_id).map_err(JobError::Seeds)?.iter().fold(
-
        (BTreeSet::new(), BTreeSet::new()),
-
        |(mut synced, mut unsynced), seed| {
-
            if seed.is_synced() {
-
                synced.insert(seed.nid);
-
            } else {
-
                unsynced.insert(seed.nid);
-
            }
-
            (synced, unsynced)
-
        },
-
    );
-

-
    let announcer = Announcer::new(AnnouncerConfig::public(
-
        *profile.id(),
-
        ReplicationFactor::MustReach(1),
-
        BTreeSet::new(),
-
        synced,
-
        unsynced,
-
    ))
-
    .map_err(|_| JobError::Announcer)?;
-

-
    node.announce(repo_id, TIMEOUT, announcer, |_, _| ())
-
        .map_err(JobError::Announce)?;
-

-
    Ok(())
-
}
-

-
#[derive(Debug, thiserror::Error)]
-
enum JobError {
-
    #[error("failed to open repository in current directory")]
-
    Cwd(#[source] radicle::rad::RemoteError),
-

-
    #[error("failed to load Radicle profile")]
-
    Profile(#[source] radicle::profile::Error),
-

-
    #[error("failed to open repository in Radicle node storage")]
-
    OpenRepository(#[source] radicle::storage::RepositoryError),
-

-
    #[error("failed to list job COBs in repository")]
-
    Jobs(#[source] radicle::storage::RepositoryError),
-

-
    #[error("failed to get job COB count")]
-
    JobCount(#[source] radicle::cob::store::Error),
-

-
    #[error("failed to get all job COBs")]
-
    AllJobs(#[source] radicle::cob::store::Error),
-

-
    #[error("failed to create a new job COB")]
-
    CreateJob(#[source] radicle::cob::store::Error),
-

-
    #[error("failed to get a job COB")]
-
    GetJob(#[source] radicle::cob::store::Error),
-

-
    #[error("failed to create a signer for Radicle repository")]
-
    Signer(#[source] radicle::profile::Error),
-

-
    #[error("didn't find job COB {0}")]
-
    NoSuchJob(JobId),
-

-
    #[error("couldn't get job when iterating")]
-
    AllJobsJob(#[source] radicle::cob::store::Error),
-

-
    #[error("failed to get mutable job COB")]
-
    GetJobMut(#[source] radicle::cob::store::Error),
-

-
    #[error("failed to add a run to a job COB")]
-
    AddRun(#[source] radicle::cob::store::Error),
-

-
    #[error("could not mark a run as finished")]
-
    Finish(#[source] radicle::cob::store::Error),
-

-
    #[error("can't serialize to JSON")]
-
    ToJson(#[source] serde_json::Error),
-

-
    #[error("failed to find job COB for oid {0}")]
-
    NoJob(Oid),
-

-
    #[error("failed to announce COB change")]
-
    Announce(#[source] radicle::node::Error),
-

-
    #[error("failed to get seeds for node")]
-
    Seeds(#[source] radicle::node::Error),
-

-
    #[error("failed to create a COB announcer")]
-
    Announcer,
-
}