radicle: remove job cob

The Job COB is defined as separate crate: [radicle-job]. So this change removes
its definition and use from the `heartwood` repository.

[radicle-job]: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z2UcCU1LgMshWvXj6hXSDDrwB8q8M
This commit is contained in:
Fintan Halpenny 2025-05-30 13:08:42 +02:00 committed by Lorenz Leutgeb
parent f4c8ff7a64
commit e6ef767f36
11 changed files with 0 additions and 1007 deletions

View File

@ -1,87 +0,0 @@
The `rad job` command lets you manage job COBs. Let's first checkout the
`heartwood` repository:
```
$ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
✓ Repository checkout successful under ./heartwood
$ cd heartwood
```
Using the `rad job` (or `rad job list`) command we can see that there are
currently no jobs listed:
```
$ rad job
Nothing to show.
```
Let's create a job to represent a new CI run. We check what the current `HEAD`
of the repository is, and use the `rad job trigger` to start a fresh job for
that commit:
```
$ git rev-parse HEAD
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
$ rad job trigger f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
╭──────────────────────────────────────────────────╮
│ Job fbbda2447c30ebbab9b746498cd41a383ff05225 │
│ Commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
│ State fresh │
╰──────────────────────────────────────────────────╯
```
Let's check the list again, and we should we see our fresh job there:
```
$ rad job
╭───────────────────────────────╮
│ ● ID Commit State │
├───────────────────────────────┤
│ ● fbbda24 f2de534 fresh │
╰───────────────────────────────╯
```
From there we can start a new job, assigning an arbitrary identifier `xyzzy`,
which would usually from the CI system that is running the job:
```
$ rad job start fbbda2447c30ebbab9b746498cd41a383ff05225 xyzzy
```
Checking the job again, we can now see that the job is `running`:
```
$ rad job
╭─────────────────────────────────╮
│ ● ID Commit State │
├─────────────────────────────────┤
│ ● fbbda24 f2de534 running │
╰─────────────────────────────────╯
$ rad job show fbbda2447c30ebbab9b746498cd41a383ff05225
╭──────────────────────────────────────────────────╮
│ Job fbbda2447c30ebbab9b746498cd41a383ff05225 │
│ Commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
│ State running │
│ Run ID xyzzy │
╰──────────────────────────────────────────────────╯
```
When a job has finished, we can mark it as done -- either with a `--success` or
`--failed` flag -- using the `rad job finish` command:
```
$ rad job finish --success fbbda2447c30ebbab9b746498cd41a383ff05225
$ rad job
╭───────────────────────────────────╮
│ ● ID Commit State │
├───────────────────────────────────┤
│ ● fbbda24 f2de534 succeeded │
╰───────────────────────────────────╯
$ rad job show fbbda2447c30ebbab9b746498cd41a383ff05225
╭──────────────────────────────────────────────────╮
│ Job fbbda2447c30ebbab9b746498cd41a383ff05225 │
│ Commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
│ State succeeded │
│ Run ID xyzzy │
╰──────────────────────────────────────────────────╯
```

View File

@ -32,8 +32,6 @@ pub mod rad_init;
pub mod rad_inspect;
#[path = "commands/issue.rs"]
pub mod rad_issue;
#[path = "commands/job.rs"]
pub mod rad_job;
#[path = "commands/ls.rs"]
pub mod rad_ls;
#[path = "commands/node.rs"]

View File

@ -25,7 +25,6 @@ const COMMANDS: &[Help] = &[
rad_inbox::HELP,
rad_inspect::HELP,
rad_issue::HELP,
rad_job::HELP,
rad_ls::HELP,
rad_node::HELP,
rad_patch::HELP,

View File

@ -1,380 +0,0 @@
#![allow(clippy::or_fun_call)]
use std::ffi::OsString;
use anyhow::{anyhow, Context as _};
use radicle::cob::job::{JobStore, Reason, State};
use radicle::crypto;
use radicle::node::device::Device;
use radicle::node::{Handle, NodeId};
use radicle::storage::{WriteRepository, WriteStorage};
use radicle::{cob, Node};
use crate::git::Rev;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::Element;
pub const HELP: Help = Help {
name: "job",
description: "Manage automated jobs on a repository",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad job [<option>...]
rad job trigger <commit-id>
rad job start <job-id> <run-id> [<url>]
rad job list
rad job show <job-id>
rad job finish <job-id> [--success | --failed]
rad job delete <job-id>
Options
--no-announce Don't announce job records to peers
--quiet, -q Don't print anything
--help Print help
"#,
};
#[derive(Default, Debug, PartialEq, Eq)]
pub enum OperationName {
Trigger,
Start,
#[default]
List,
Show,
Finish,
Delete,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Operation {
Trigger {
commit: Rev,
},
Start {
job_id: Rev,
run_id: String,
info_url: Option<String>,
},
List,
Show {
job_id: Rev,
},
Finish {
job_id: Rev,
reason: Reason,
},
Delete {
job_id: Rev,
},
}
#[derive(Debug)]
pub struct Options {
pub op: Operation,
pub announce: bool,
pub quiet: bool,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<OperationName> = None;
let mut commit: Option<Rev> = None;
let mut job_id: Option<Rev> = None;
let mut run_id: Option<String> = None;
let mut info_url: Option<String> = None;
let mut announce = true;
let mut quiet = false;
let mut succeeded = false;
let mut failed = false;
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
Long("no-announce") => {
announce = false;
}
Long("quiet") | Short('q') => {
quiet = true;
}
Long("success") | Long("succeeded") | Short('s') => {
succeeded = true;
}
Long("failure") | Long("failed") | Short('f') => {
failed = true;
}
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
"trigger" => op = Some(OperationName::Trigger),
"start" => op = Some(OperationName::Start),
"list" => op = Some(OperationName::List),
"show" => op = Some(OperationName::Show),
"finish" => op = Some(OperationName::Finish),
"delete" => op = Some(OperationName::Delete),
unknown => anyhow::bail!("unknown operation '{}'", unknown),
},
Value(val) if commit.is_none() && op == Some(OperationName::Trigger) => {
let val = term::args::oid(&val)?;
let val = Rev::from(val.to_string());
commit = Some(val);
}
Value(val)
if job_id.is_none()
&& op.is_some()
&& matches!(
op.as_ref().unwrap(),
OperationName::Start
| OperationName::Show
| OperationName::Finish
| OperationName::Delete
) =>
{
let val = term::args::oid(&val)?;
let val = Rev::from(val.to_string());
job_id = Some(val);
}
Value(val)
if job_id.is_some()
&& run_id.is_none()
&& op.is_some()
&& matches!(op.as_ref().unwrap(), OperationName::Start) =>
{
run_id = Some(val.to_str().unwrap().to_string());
}
Value(val)
if job_id.is_some()
&& run_id.is_some()
&& op.is_some()
&& matches!(op.as_ref().unwrap(), OperationName::Start) =>
{
info_url = Some(val.to_str().unwrap().to_string());
}
_ => {
return Err(anyhow!(arg.unexpected()));
}
}
}
let op = match op.unwrap_or_default() {
OperationName::Trigger => Operation::Trigger {
commit: commit.ok_or_else(|| anyhow!("a commit id must be provided"))?,
},
OperationName::Start => Operation::Start {
job_id: job_id.ok_or_else(|| anyhow!("a job id must be provided"))?,
run_id: run_id.ok_or_else(|| anyhow!("a run id must be provided"))?,
info_url,
},
OperationName::List => Operation::List,
OperationName::Show => Operation::Show {
job_id: job_id.ok_or_else(|| anyhow!("a job id must be provided"))?,
},
OperationName::Finish => Operation::Finish {
job_id: job_id.ok_or_else(|| anyhow!("a job id must be provided"))?,
reason: if !succeeded && !failed {
return Err(anyhow!("must give one of --success or --failure"))?;
} else if succeeded && failed {
return Err(anyhow!("must give one of --success or --failure, not both"))?;
} else if succeeded {
Reason::Succeeded
} else {
Reason::Failed
},
},
OperationName::Delete => Operation::Delete {
job_id: job_id.ok_or_else(|| anyhow!("a job id to remove must be provided"))?,
},
};
Ok((
Options {
op,
announce,
quiet,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let (_, rid) = radicle::rad::cwd()?;
let repo = profile.storage.repository_mut(rid)?;
let announce = options.announce
&& matches!(
&options.op,
Operation::Trigger { .. }
| Operation::Start { .. }
| Operation::Finish { .. }
| Operation::Delete { .. }
);
let mut node = Node::new(profile.socket());
let mut ci_store = JobStore::open(&repo)?;
match options.op {
Operation::Trigger { commit } => {
let signer = term::signer(&profile)?;
trigger(&commit, &mut ci_store, &repo, &signer, options.quiet)?;
}
Operation::Start {
job_id,
run_id,
info_url,
} => {
let signer = term::signer(&profile)?;
start(&job_id, &run_id, info_url, &mut ci_store, &repo, &signer)?;
}
Operation::List => {
list(&ci_store)?;
}
Operation::Show { job_id } => {
show(&job_id, &ci_store, &repo)?;
}
Operation::Finish { job_id, reason } => {
let signer = term::signer(&profile)?;
finish(&job_id, reason, &mut ci_store, &repo, &signer)?;
}
Operation::Delete { job_id } => {
let signer = term::signer(&profile)?;
let job_id = job_id.resolve(&repo.backend)?;
ci_store.remove(&job_id, &signer)?;
}
}
if announce {
match node.announce_refs(rid) {
Ok(_) => {}
Err(e) if e.is_connection_err() => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
fn trigger<R, G>(
commit: &Rev,
store: &mut JobStore<R>,
repo: &radicle::storage::git::Repository,
signer: &Device<G>,
quiet: bool,
) -> anyhow::Result<()>
where
R: WriteRepository + cob::Store<Namespace = NodeId>,
G: crypto::signature::Signer<crypto::Signature>,
{
let commit = commit.resolve(&repo.backend)?;
let job = store.create(commit, signer)?;
if !quiet {
term::job::show(&job, job.id())?;
}
Ok(())
}
fn start<R, G>(
job_id: &Rev,
run_id: &str,
info_url: Option<String>,
store: &mut JobStore<R>,
repo: &radicle::storage::git::Repository,
signer: &Device<G>,
) -> anyhow::Result<()>
where
R: WriteRepository + cob::Store<Namespace = NodeId>,
G: crypto::signature::Signer<crypto::Signature>,
{
let job_id = job_id.resolve(&repo.backend)?;
let mut job = store.get_mut(&job_id)?;
job.start(run_id.to_string(), info_url, signer)?;
Ok(())
}
// TODO: This should use the COB cache for performance.
fn list<R: WriteRepository + cob::Store<Namespace = NodeId>>(
store: &JobStore<R>,
) -> anyhow::Result<()> {
if store.is_empty()? {
term::print(term::format::italic("Nothing to show."));
return Ok(());
}
let mut table = term::Table::new(term::table::TableOptions::bordered());
table.header([
term::format::dim(String::from("")),
term::format::bold(String::from("ID")),
term::format::bold(String::from("Commit")),
term::format::bold(String::from("State")),
]);
table.divider();
for result in store.all()? {
let Ok((id, ci)) = result else {
// Skip COBs that failed to load.
continue;
};
table.push([
match ci.state() {
State::Fresh => term::format::positive("").into(),
State::Running => term::format::positive("").into(),
State::Finished(Reason::Succeeded) => term::format::positive("").into(),
State::Finished(Reason::Failed) => term::format::negative("").into(),
},
term::format::tertiary(term::format::cob(&id).to_string()),
term::format::tertiary(term::format::oid(ci.commit()).to_string()),
term::format::tertiary(term::format::job_state(ci.state()).to_string()),
]);
}
if table.is_empty() {
term::print(term::format::dim("No jobs to show."));
} else {
table.print();
}
Ok(())
}
fn show<R: WriteRepository + cob::Store<Namespace = NodeId>>(
job_id: &Rev,
store: &JobStore<R>,
repo: &radicle::storage::git::Repository,
) -> anyhow::Result<()> {
let job_id = job_id.resolve(&repo.backend)?;
let job = store
.get(&job_id)?
.context("No job with the given ID exists")?;
term::job::show(&job, &job_id)?;
Ok(())
}
fn finish<R, G>(
job_id: &Rev,
reason: Reason,
store: &mut JobStore<R>,
repo: &radicle::storage::git::Repository,
signer: &Device<G>,
) -> anyhow::Result<()>
where
R: WriteRepository + cob::Store<Namespace = NodeId>,
G: crypto::signature::Signer<crypto::Signature>,
{
let job_id = job_id.resolve(&repo.backend)?;
let mut job = store.get_mut(&job_id)?;
job.finish(reason, signer)?;
Ok(())
}

View File

@ -232,13 +232,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(),
);
}
"job" => {
term::run_command_args::<rad_job::Options, _>(
rad_job::HELP,
rad_job::run,
args.to_vec(),
);
}
"ls" => {
term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
}

View File

@ -2,7 +2,6 @@ pub mod args;
pub use args::{Args, Error, Help};
pub mod format;
pub mod io;
pub mod job;
pub use io::signer;
pub mod cob;
pub mod comment;

View File

@ -30,11 +30,6 @@ pub fn oid(oid: impl Into<radicle::git::Oid>) -> Paint<String> {
Paint::new(format!("{:.7}", oid.into()))
}
/// Format a job COB state.
pub fn job_state(state: radicle::cob::job::State) -> Paint<String> {
Paint::new(format!("{}", state))
}
/// Wrap parenthesis around styled input, eg. `"input"` -> `"(input)"`.
pub fn parens<D: fmt::Display>(input: Paint<D>) -> Paint<String> {
Paint::new(format!("({})", input.item)).with_style(input.style)

View File

@ -1,51 +0,0 @@
use radicle::cob;
use radicle::cob::job;
use radicle_term::table::TableOptions;
use radicle_term::{Table, VStack};
use crate::terminal as term;
use crate::terminal::Element;
pub fn show(job: &job::Job, id: &cob::ObjectId) -> anyhow::Result<()> {
let mut attrs = Table::<2, term::Line>::new(TableOptions {
spacing: 2,
..TableOptions::default()
});
attrs.push([
term::format::tertiary("Job".to_owned()).into(),
term::format::bold(id.to_string()).into(),
]);
attrs.push([
term::format::tertiary("Commit".to_owned()).into(),
term::format::bold(job.commit().to_owned()).into(),
]);
attrs.push([
term::format::tertiary("State".to_owned()).into(),
term::format::bold(job.state().to_string()).into(),
]);
if let Some(run_id) = job.run_id() {
attrs.push([
term::format::tertiary("Run ID".to_owned()).into(),
term::format::bold(run_id.to_string()).into(),
]);
}
if let Some(info_url) = job.info_url() {
attrs.push([
term::format::tertiary("Info URL".to_owned()).into(),
term::format::bold(info_url.to_string()).into(),
]);
}
let widget = VStack::default()
.border(Some(term::colors::FAINT))
.child(attrs);
widget.print();
Ok(())
}

View File

@ -3159,17 +3159,3 @@ fn rad_workflow() {
)
.unwrap();
}
#[test]
fn rad_job() {
let mut environment = Environment::new();
let profile = environment.profile(config::profile("alice"));
let home = &profile.home;
let working = environment.tmp().join("working");
// Setup a test repository.
fixtures::repository(&working);
test("examples/rad-init.md", &working, Some(home), []).unwrap();
test("examples/rad-job.md", &working, Some(home), []).unwrap();
}

View File

@ -4,7 +4,6 @@ pub mod common;
pub mod external;
pub mod identity;
pub mod issue;
pub mod job;
pub mod op;
pub mod patch;
pub mod store;

View File

@ -1,458 +0,0 @@
//! Track "jobs" related to a repository.
//!
//! The purpose of this COB is to allow users of Radicle to have a way
//! of keeping track of what automated processing of changes to a
//! repository have been done. A "job" might be a continuous
//! integration automation building the software in a repository and
//! running its automated tests. A delegate for the repository could
//! track COBs emitted by trusted nodes to help with deciding when a
//! patch is ready for them to merge.
use std::{ops::Deref, str::FromStr};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::cob;
use crate::cob::change::store::Entry;
use crate::cob::store;
use crate::cob::store::{Cob, CobAction, Store, Transaction};
use crate::cob::{EntryId, ObjectId, TypeName};
use crate::crypto::ssh::ExtendedSignature;
use crate::git;
use crate::node::device::Device;
use crate::node::NodeId;
use crate::prelude::ReadRepository;
use crate::storage::{Oid, WriteRepository};
use super::store::CobWithType;
/// The name of this COB type. Note that this is a "beta" COB, which
/// means it's not meant for others to rely on yet, and we may change
/// it without warning.
pub static TYPENAME: Lazy<TypeName> =
Lazy::new(|| FromStr::from_str("xyz.radicle.beta.job").expect("type name is valid"));
/// An identifier for the job.
pub type JobId = ObjectId;
/// All the possible errors from this type of COB.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("initialization failed: {0}")]
Init(&'static str),
#[error("op decoding failed: {0}")]
Op(#[from] cob::op::OpEncodingError),
#[error("store: {0}")]
Store(#[from] store::Error),
#[error("can't trigger a job which is not fresh")]
TriggerWhenNotFresh,
#[error("can't start a job which is not triggered")]
StartWhenNotFresh,
#[error("can't finish a job which is not running")]
FinishWhenNotRunning,
}
/// All the possible states of this COB.
///
/// This is primarily modeled for CI, for now. A COB is created by a
/// node when it triggers a CI run, and then updated when the CI
/// system actually starts executing the run, and finishes. The CI
/// system assigns an identifier to the run, and may have a URL for
/// the log. These can also be stored in the COB.
///
/// This COB is essentially a state machine that tracks the state of
/// an automated run Ci. When the COB is created, in state `Fresh`, it
/// just records the git commit the run uses. This can't be changed.
/// The COB may be created before the run actually starts. Once the
/// run starts, the COB state changes to `Running`. When the run
/// finished, the state changes to `Finished`, and the state records
/// why the run finished: `Succeeded` or `Failed`.
///
/// No other state changes are allowed for the COB.
///
/// Note that if CI runs again for the same commit, a new COB is
/// created. The two runs may result in different outcomes, even if
/// nothing in the source code has changed. For example, the CI system
/// may run out of disk space, or use different versions of the
/// software used in the run.
#[derive(Debug, Default, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "status")]
pub enum State {
/// COB has been created, job has not yet started running.
#[default]
Fresh,
/// Job has started running.
Running,
/// Job has finished.
Finished(Reason),
}
impl std::fmt::Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fresh => write!(f, "fresh"),
Self::Running => write!(f, "running"),
Self::Finished(Reason::Succeeded) => write!(f, "succeeded"),
Self::Finished(Reason::Failed) => write!(f, "failed"),
}
}
}
/// Why did build finish?
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
pub enum Reason {
/// Build was successful.
Succeeded,
/// Build failed for some reason.
Failed,
}
/// Actions to update this COB.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Action {
/// Initialize the COB for a new job.
Trigger { commit: git::Oid },
/// Start a job.
Start {
run_id: String,
info_url: Option<String>,
},
/// Finish a job.
Finish { reason: Reason },
}
impl CobAction for Action {}
/// Type of COB operation.
pub type Op = cob::Op<Action>;
/// The COB with actions applied.
///
/// A job is based on a specific commit. This is set when the COB is
/// created and can't be changed.
///
/// A job has a specific [`State`].
///
/// A job may have a "run id", which is an arbitrary string. It might
/// be the identifier for a CI run, set by an external CI system, for
/// example. The id is informational and there no guarantees what it
/// means, or that it's unique.
///
/// A job may also store a URL to more information. This might be a
/// link to a run log in a CI system, for example.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Job {
commit: git::Oid,
state: State,
run_id: Option<String>,
info_url: Option<String>,
}
impl Job {
/// Create a new `Job` in the `Fresh` state, using the provided `commit`.
fn new(commit: git::Oid) -> Self {
Self {
commit,
state: State::default(),
run_id: None,
info_url: None,
}
}
/// Get the commit that this `Job` was created with.
pub fn commit(&self) -> git::Oid {
self.commit
}
/// Get the run identifier, if any, that was associated with this `Job`.
pub fn run_id(&self) -> Option<&str> {
self.run_id.as_deref()
}
/// Get the info URL, if any, that was associated with this `Job`.
pub fn info_url(&self) -> Option<&str> {
self.info_url.as_deref()
}
/// Get the `State` of this `Job`.
pub fn state(&self) -> State {
self.state
}
/// Apply a single action to the job.
fn action(&mut self, action: Action) -> Result<(), Error> {
match action {
Action::Trigger { .. } => {
if self.state != State::Fresh {
return Err(Error::TriggerWhenNotFresh);
}
}
Action::Start { run_id, info_url } => {
if self.state != State::Fresh {
return Err(Error::StartWhenNotFresh);
}
self.state = State::Running;
self.run_id = Some(run_id);
self.info_url = info_url;
}
Action::Finish { reason } => {
if self.state != State::Running {
return Err(Error::FinishWhenNotRunning);
}
self.state = State::Finished(reason);
}
}
Ok(())
}
}
impl Cob for Job {
type Action = Action;
type Error = Error;
fn from_root<R: ReadRepository>(op: Op, _repo: &R) -> Result<Self, Self::Error> {
let mut actions = op.actions.into_iter();
let Some(Action::Trigger { commit }) = actions.next() else {
return Err(Error::Init("the first action must be of type `trigger`"));
};
let mut job = Job::new(commit);
for action in actions {
job.action(action)?;
}
Ok(job)
}
fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
&mut self,
op: Op,
_concurrent: I,
_repo: &R,
) -> Result<(), Error> {
// Some day this needs to check authorization. However, we
// don't yet know what the rules should be.
for action in op.actions {
self.action(action)?;
}
Ok(())
}
}
impl CobWithType for Job {
fn type_name() -> &'static TypeName {
&TYPENAME
}
}
impl<R: ReadRepository> cob::Evaluate<R> for Job {
type Error = Error;
fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
let op = Op::try_from(entry)?;
let job = Job::from_root(op, repo)?;
Ok(job)
}
fn apply<'a, I>(
&mut self,
entry: &cob::Entry,
concurrent: I,
repo: &R,
) -> Result<(), Self::Error>
where
I: Iterator<Item = (&'a Oid, &'a Entry<Oid, Oid, ExtendedSignature>)>,
{
let op = Op::try_from(entry)?;
self.op(op, concurrent.map(|(_, e)| e), repo)
}
}
impl<R: ReadRepository> Transaction<Job, R> {
/// Push an [`Action::Trigger`] which will create a new `Job` with the
/// provided `commit` in the [`State::Fresh`] state.
pub fn trigger(&mut self, commit: git::Oid) -> Result<(), store::Error> {
self.push(Action::Trigger { commit })
}
/// Push an [`Action::Start`] which will start the `Job` with the provided
/// metadata and move the `Job` into the [`State::Running`] state.
pub fn start(&mut self, run_id: String, info_url: Option<String>) -> Result<(), store::Error> {
self.push(Action::Start { run_id, info_url })
}
/// Push an [`Action::Finish`] which will finish the `Job` with the provided
/// reason, moving the `Job` into the [`State::Finished`] state.
pub fn finish(&mut self, reason: Reason) -> Result<(), store::Error> {
self.push(Action::Finish { reason })
}
}
pub struct JobMut<'a, 'g, R> {
id: ObjectId,
job: Job,
store: &'g mut JobStore<'a, R>,
}
impl<'a, 'g, R> From<JobMut<'a, 'g, R>> for (JobId, Job) {
fn from(value: JobMut<'a, 'g, R>) -> Self {
(value.id, value.job)
}
}
impl<R> std::fmt::Debug for JobMut<'_, '_, R> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("JobMut")
.field("id", &self.id)
.field("job", &self.job)
.finish()
}
}
impl<R> JobMut<'_, '_, R>
where
R: WriteRepository + cob::Store<Namespace = NodeId>,
{
/// Reload the COB from storage.
pub fn reload(&mut self) -> Result<(), store::Error> {
self.job = self
.store
.get(&self.id)?
.ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
Ok(())
}
pub fn id(&self) -> &ObjectId {
&self.id
}
/// Transition the `Job` into a running state, storing the provided
/// metadata.
pub fn start<G>(
&mut self,
run_id: String,
info_url: Option<String>,
signer: &Device<G>,
) -> Result<EntryId, Error>
where
G: crypto::signature::Signer<crypto::Signature>,
{
self.transaction("Start", signer, |tx| {
tx.start(run_id, info_url)?;
Ok(())
})
}
/// Transition the `Job` into a finished state, with the provided `reason`.
pub fn finish<G>(&mut self, reason: Reason, signer: &Device<G>) -> Result<EntryId, Error>
where
G: crypto::signature::Signer<crypto::Signature>,
{
self.transaction("Finish", signer, |tx| tx.finish(reason))
}
pub fn transaction<G, F>(
&mut self,
message: &str,
signer: &Device<G>,
operations: F,
) -> Result<EntryId, Error>
where
G: crypto::signature::Signer<crypto::Signature>,
F: FnOnce(&mut Transaction<Job, R>) -> Result<(), store::Error>,
{
let mut tx = Transaction::default();
operations(&mut tx)?;
let (job, id) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
self.job = job;
Ok(id)
}
}
impl<R> Deref for JobMut<'_, '_, R> {
type Target = Job;
fn deref(&self) -> &Self::Target {
&self.job
}
}
pub struct JobStore<'a, R> {
raw: Store<'a, Job, R>,
}
impl<'a, R> Deref for JobStore<'a, R> {
type Target = Store<'a, Job, R>;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
impl<'a, R> JobStore<'a, R>
where
R: WriteRepository + ReadRepository + cob::Store<Namespace = NodeId>,
{
pub fn open(repository: &'a R) -> Result<Self, store::Error> {
let raw = store::Store::open(repository)?;
Ok(Self { raw })
}
/// Get the `Job`, if any, identified by `id`.
pub fn get(&self, id: &JobId) -> Result<Option<Job>, store::Error> {
self.raw.get(id)
}
/// Get the `Job`, identified by `id`, which can be mutated.
///
/// # Errors
///
/// This will fail if the `Job` could not be found.
pub fn get_mut<'g>(&'g mut self, id: &JobId) -> Result<JobMut<'a, 'g, R>, store::Error> {
let job = self
.raw
.get(id)?
.ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), *id))?;
Ok(JobMut {
id: *id,
job,
store: self,
})
}
/// Create a fresh `Job` with the provided `commit_id`.
pub fn create<'g, G>(
&'g mut self,
commit_id: git::Oid,
signer: &Device<G>,
) -> Result<JobMut<'a, 'g, R>, Error>
where
G: crypto::signature::Signer<crypto::Signature>,
{
let (id, job) = Transaction::initial("Create job", &mut self.raw, signer, |tx, _| {
tx.trigger(commit_id)?;
Ok(())
})?;
Ok(JobMut {
id,
job,
store: self,
})
}
/// Delete the `Job` identified by `id`.
pub fn remove<G>(&self, id: &JobId, signer: &Device<G>) -> Result<(), store::Error>
where
G: crypto::signature::Signer<crypto::Signature>,
{
self.raw.remove(id, signer)
}
}