Radish alpha
r
Radicle CI broker
Radicle
Git (anonymous pull)
Log in to clone via SSH
feat: cibtool can add every type of CI event to event queue
Lars Wirzenius committed 1 year ago
commit 1bc0ad40dc9aeb2c4631f86b6305869105d45bce
parent 46651b7fa55e20a13ad3f312e88a38decd81f3c6
8 files changed +473 -28
modified ci-broker.md
@@ -314,6 +314,98 @@ then stdout contains ""id": "xyzzy""
~~~


+
## Runs adapter on each type of event
+

+
_Want:_ CI broker runs the adapter for each type of CI event.
+

+
_Why:_ The adapter needs to handle each type of CI event.
+

+
_Who:_ `cib-devs`
+

+
We verify this by adding CI events to the event queue using `cibtool`
+
and checking that `cib` can process those. This is simpler and more
+
direct than emitting node events that result in the desired CI events.
+
We are here not concerned about whether `cib` handles node events or
+
turns those into the correct CI events: we verify that in other ways.
+

+
We first set things up, including creating a repository `xyzzy`, and a
+
Radicle patch in that repository. The id of the patch is in the file
+
`patch-id.txt` so that it can be used.
+

+
~~~scenario
+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+
given a directory reports
+
when I run ./env.sh env -C xyzzy git switch -c branchy
+
given file create-patch
+
when I run ./env.sh env -C xyzzy bash ../create-patch ../patch-id.txt
+
~~~
+

+
Verify that `cib` can process a branch creation event.
+

+
~~~
+
when I run rm -f ci-broker.db
+
when I run cibtool --db ci-broker.db event add --repo xyzzy --ref main --commit HEAD --kind branch-created --id-file id.txt
+
when I run ./env.sh cib --config broker.yaml queued
+
when I run cibtool --db ci-broker.db run list
+
then stdout has one line
+
~~~
+

+
Verify that `cib` can process a branch update event.
+

+
~~~
+
when I run rm -f ci-broker.db
+
when I run cibtool --db ci-broker.db event add --repo xyzzy --ref brancy --commit HEAD --base main --kind branch-updated --id-file id.txt
+
when I run ./env.sh cib --config broker.yaml queued
+
when I run cibtool --db ci-broker.db run list
+
then stdout has one line
+
~~~
+

+
Verify that `cib` can process a branch deletion event.
+

+
~~~
+
when I run rm -f ci-broker.db
+
when I run cibtool --db ci-broker.db event add --repo xyzzy --ref main --commit HEAD --kind branch-deleted --id-file id.txt
+
when I run ./env.sh cib --config broker.yaml queued
+
when I run cibtool --db ci-broker.db run list
+
then stdout has one line
+
~~~
+

+
Verify that `cib` can process a patch creation event.
+

+
~~~
+
when I run rm -f ci-broker.db
+
when I run cibtool --db ci-broker.db event add --repo xyzzy --ref main --commit HEAD --kind patch-created --patch-id-file patch-id.txt --id-file id.txt
+
when I run ./env.sh cib --config broker.yaml queued
+
when I run cibtool --db ci-broker.db run list
+
then stdout has one line
+
~~~
+

+
Verify that `cib` can process a patch update event.
+

+
~~~
+
when I run rm -f ci-broker.db
+
when I run cibtool --db ci-broker.db event add --repo xyzzy --ref main --commit HEAD --kind patch-updated --patch-id-file patch-id.txt --id-file id.txt
+
when I run ./env.sh cib --config broker.yaml queued
+
when I run cibtool --db ci-broker.db run list
+
then stdout has one line
+
~~~
+

+
~~~{#create-patch .file .sh}
+
#!/bin/bash
+
set -euo pipefail
+

+
touch foo
+
git add foo
+
git commit -m foo
+
EDITOR=/bin/true git push rad HEAD:refs/patches
+

+
rad patch list |
+
	awk 'NR == 4 { print $3 }' |
+
	xargs rad patch show |
+
	awk 'NR == 3 { print $3 }' >"$1"
+
~~~
+

## Reports it version

_Want:_ `cib` and `cibtool` report their version, if invoked with the
@@ -752,7 +844,7 @@ down.
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
given a Git repository testy in the Radicle node

-
when I run ./env.sh cibtool --db ci-broker.db event add --repo testy --ref main --commit HEAD
+
when I run ./env.sh cibtool --db ci-broker.db event add --repo testy --ref main --commit HEAD --kind branch-updated --base main
when I run cibtool --db ci-broker.db event shutdown

given a directory reports
@@ -847,6 +939,7 @@ done

echo OK
~~~
+

# Acceptance criteria for management tool

The `cibtool` management tool can be used to examine and change the CI
@@ -874,7 +967,7 @@ given a Git repository xyzzy in the Radicle node
when I run cibtool --db x.db event list
then stdout is empty

-
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref main --commit HEAD --base HEAD --id-file id.txt
+
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref main --commit HEAD --base HEAD --id-file id.txt --kind branch-updated

when I run cibtool --db x.db event show --id-file id.txt
then stdout contains "rad:"
@@ -903,12 +996,12 @@ given a Git repository testy in the Radicle node
when I run cibtool --db x.db event list
then stdout is empty

-
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD
-
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD
-
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD
-
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD
-
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD
-
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD
+
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD --kind branch-updated
+
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD --kind branch-updated
+
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD --kind branch-updated
+
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD --kind branch-updated
+
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD --kind branch-updated
+
when I run ./env.sh cibtool --db x.db event add --repo testy --ref main --commit HEAD --base HEAD --kind branch-updated

when I run cibtool --db x.db event remove --all
when I run cibtool --db x.db event list
@@ -936,6 +1029,106 @@ when I run cibtool --db x.db event shutdown --id-file id.txt
when I run cibtool --db x.db event show --id-file id.txt
then stdout contains "Shutdown"
~~~
+
## Can add a branch creation event to queue
+

+
_Want:_ `cibtool` can add an event for branch being created to the
+
queued events.
+

+
_Why:_ This is needed for testing.
+

+
_Who:_ `cib-devs`
+

+
~~~scenario
+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+

+
when I run ./env.sh env -C xyzzy git switch -c oksa
+
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref oksa --commit HEAD --kind branch-created
+

+
when I run cibtool --db x.db event list --json
+
then stdout contains "BranchCreated"
+
~~~
+

+
## Can add a branch update event to queue
+

+
_Want:_ `cibtool` can add an event for branch being updated to the
+
queued events.
+

+
_Why:_ This is needed for testing.
+

+
_Who:_ `cib-devs`
+

+
~~~scenario
+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+

+
when I run ./env.sh env -C xyzzy git switch -c oksa
+
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref oksa --commit HEAD --kind branch-updated --base HEAD
+

+
when I run cibtool --db x.db event list --json
+
then stdout contains "BranchUpdated"
+
~~~
+

+
## Can add a branch deletion event to queue
+

+
_Want:_ `cibtool` can add an event for branch being deleted to the
+
queued events.
+

+
_Why:_ This is needed for testing.
+

+
_Who:_ `cib-devs`
+

+
~~~scenario
+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+

+
when I run ./env.sh env -C xyzzy git switch -c oksa
+
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref oksa --commit HEAD --kind branch-deleted
+

+
when I run cibtool --db x.db event list --json
+
then stdout contains "BranchDeleted"
+
~~~
+

+
## Can add a patch creation event to queue
+

+
_Want:_ `cibtool` can add an event for a branch being created to the
+
queued events.
+

+
_Why:_ This is needed for testing.
+

+
_Who:_ `cib-devs`
+

+
~~~scenario
+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+

+
when I run ./env.sh env -C xyzzy git switch -c oksa
+
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref oksa --commit HEAD --kind patch-created --patch-id f863364f6774160607d90811b06a0e401c097466
+

+
when I run cibtool --db x.db event list --json
+
then stdout contains "PatchCreated"
+
~~~
+

+
## Can add a patch update event to queue
+

+
_Want:_ `cibtool` can add an event for a branch being updated to the
+
queued events.
+

+
_Why:_ This is needed for testing.
+

+
_Who:_ `cib-devs`
+

+
~~~scenario
+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+

+
when I run ./env.sh env -C xyzzy git switch -c oksa
+
when I run ./env.sh cibtool --db x.db event add --repo xyzzy --ref oksa --commit HEAD --kind patch-updated --patch-id f863364f6774160607d90811b06a0e401c097466
+

+
when I run cibtool --db x.db event list --json
+
then stdout contains "PatchUpdated"
+
~~~
+

## Can trigger a CI run

_Want:_ The node operator can easily trigger a CI run without
@@ -1113,11 +1306,11 @@ Note that we verify both lookup by name and by repository ID, and by
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
given a Git repository testy in the Radicle node

-
when I try to run ./env.sh cibtool --db x.db event add --repo missing --ref main --commit HEAD --base c0ffee
+
when I try to run ./env.sh cibtool --db x.db event add --repo missing --ref main --commit HEAD --base c0ffee --kind branch-updated
then command fails
then stderr contains "missing"

-
when I try to run ./env.sh cibtool --db x.db event add --repo rad:z3byzFpcfbMJBp4tKYyuuTZiP8WUB --ref main --commit HEAD --base c0ffee
+
when I try to run ./env.sh cibtool --db x.db event add --repo rad:z3byzFpcfbMJBp4tKYyuuTZiP8WUB --ref main --commit HEAD --base c0ffee --kind branch-updated
then command fails
then stderr contains "rad:z3byzFpcfbMJBp4tKYyuuTZiP8WUB"

modified src/bin/cibtool.rs
@@ -352,8 +352,19 @@ enum CibToolError {
    #[error("failed to read filters from YAML file {0}")]
    ReadFilters(PathBuf, #[source] radicle_ci_broker::filter::FilterError),

-
    #[error("failed to construct a CiEvent::BranchCreated")]
-
    BranchCreted(#[source] CiEventError),
+
    #[error("when adding a branch-create event, the --base option is not allowed")]
+
    NoBaseAllowed,
+

+
    #[error("when adding a branch-update event, the --base option is required")]
+
    BaseRequired,
+

+
    #[error(
+
        "when adding a patch-create or patch--update event, the --patch-id option is required"
+
    )]
+
    PatchIdRequired,
+

+
    #[error("failed to construct a CI event")]
+
    CiEvent(#[source] CiEventError),

    #[error("programming error: failed to set up inter-thread notification channel")]
    Notification(#[source] NotificationError),
modified src/bin/cibtoolcmd/event.rs
@@ -1,7 +1,12 @@
use std::io::Write;

+
use clap::ValueEnum;
+

+
use radicle::patch::PatchId;
#[allow(unused_imports)] // FIXME
-
use radicle_ci_broker::{filter::EventFilter, node_event_source::NodeEventSource};
+
use radicle_ci_broker::{
+
    filter::EventFilter, node_event_source::NodeEventSource, util::read_file_as_objectid,
+
};

use super::*;

@@ -81,11 +86,23 @@ pub struct AddEvent {
    #[clap(long)]
    commit: String,

+
    /// Type of event to create.
+
    #[clap(long)]
+
    kind: EventKind,
+

    /// The base commit referred to by the event. The value is parsed
    /// the same way as `--commit` value.
    #[clap(long)]
    base: Option<String>,

+
    // The patch id to use for a patch event.
+
    #[clap(long)]
+
    patch_id: Option<PatchId>,
+

+
    // Read the patch id to use for a patch event from this file.
+
    #[clap(long)]
+
    patch_id_file: Option<PathBuf>,
+

    /// Write the event to this file, as JSON, instead of adding it to
    /// the queue.
    #[clap(long)]
@@ -167,16 +184,54 @@ impl Leaf for AddEvent {
        let name =
            RefString::try_from(name.clone()).map_err(|e| CibToolError::RefString(name, e))?;

-
        let event = if let Some(base) = &self.base {
-
            let base = if let Ok(base) = Oid::from_str(base) {
-
                base
-
            } else {
-
                self.lookup_commit(rid, base)?
-
            };
-
            CiEvent::branch_updated(nid, rid, &name, oid, base)
-
                .map_err(CibToolError::BranchCreted)?
-
        } else {
-
            CiEvent::branch_created(nid, rid, &name, oid).map_err(CibToolError::BranchCreted)?
+
        let event = match &self.kind {
+
            EventKind::BranchCreated => {
+
                if self.base.is_some() {
+
                    return Err(CibToolError::NoBaseAllowed);
+
                } else {
+
                    CiEvent::branch_created(nid, rid, &name, oid).map_err(CibToolError::CiEvent)?
+
                }
+
            }
+
            EventKind::BranchUpdated => {
+
                if let Some(base) = &self.base {
+
                    let base = if let Ok(base) = Oid::from_str(base) {
+
                        base
+
                    } else {
+
                        self.lookup_commit(rid, base)?
+
                    };
+
                    CiEvent::branch_updated(nid, rid, &name, oid, base)
+
                        .map_err(CibToolError::CiEvent)?
+
                } else {
+
                    return Err(CibToolError::BaseRequired);
+
                }
+
            }
+
            EventKind::BranchDeleted => {
+
                CiEvent::branch_deleted(nid, rid, &name, oid).map_err(CibToolError::CiEvent)?
+
            }
+
            EventKind::PatchCreated => {
+
                if let Some(patch_id) = &self.patch_id {
+
                    CiEvent::patch_created(nid, rid, *patch_id, oid)
+
                        .map_err(CibToolError::CiEvent)?
+
                } else if let Some(filename) = &self.patch_id_file {
+
                    let patch_id = read_file_as_objectid(filename)?;
+
                    CiEvent::patch_created(nid, rid, patch_id, oid)
+
                        .map_err(CibToolError::CiEvent)?
+
                } else {
+
                    return Err(CibToolError::PatchIdRequired);
+
                }
+
            }
+
            EventKind::PatchUpdated => {
+
                if let Some(patch_id) = &self.patch_id {
+
                    CiEvent::patch_updated(nid, rid, *patch_id, oid)
+
                        .map_err(CibToolError::CiEvent)?
+
                } else if let Some(filename) = &self.patch_id_file {
+
                    let patch_id = read_file_as_objectid(filename)?;
+
                    CiEvent::patch_created(nid, rid, patch_id, oid)
+
                        .map_err(CibToolError::CiEvent)?
+
                } else {
+
                    return Err(CibToolError::PatchIdRequired);
+
                }
+
            }
        };

        if let Some(output) = &self.output {
@@ -198,6 +253,15 @@ impl Leaf for AddEvent {
    }
}

+
#[derive(Debug, Clone, ValueEnum)]
+
enum EventKind {
+
    BranchCreated,
+
    BranchUpdated,
+
    BranchDeleted,
+
    PatchCreated,
+
    PatchUpdated,
+
}
+

/// Show an event in the queue.
#[derive(Parser)]
pub struct ShowEvent {
modified src/bin/cibtoolcmd/trigger.rs
@@ -35,8 +35,8 @@ impl Leaf for TriggerCmd {
        let name = format!("refs/namespaces/{nid}/refs/heads/{}", self.name.as_str());
        let name =
            RefString::try_from(name.clone()).map_err(|e| CibToolError::RefString(name, e))?;
-
        let event = CiEvent::branch_updated(nid, rid, &name, oid, base)
-
            .map_err(CibToolError::BranchCreted)?;
+
        let event =
+
            CiEvent::branch_updated(nid, rid, &name, oid, base).map_err(CibToolError::CiEvent)?;

        let db = args.open_db()?;
        let id = db.push_queued_ci_event(event)?;
modified src/ci_event.rs
@@ -91,6 +91,51 @@ impl CiEvent {
        }))
    }

+
    pub fn branch_deleted(
+
        node: NodeId,
+
        repo: RepoId,
+
        branch: &str,
+
        tip: Oid,
+
    ) -> Result<Self, CiEventError> {
+
        let branch =
+
            namespaced_branch(branch).map_err(|_| CiEventError::without_namespace2(branch))?;
+
        Ok(Self::V1(CiEventV1::BranchDeleted {
+
            from_node: node,
+
            repo,
+
            branch: RefString::try_from(branch.clone())
+
                .map_err(|e| CiEventError::RefString(branch.clone(), e))?,
+
            tip,
+
        }))
+
    }
+

+
    pub fn patch_created(
+
        node: NodeId,
+
        repo: RepoId,
+
        patch: PatchId,
+
        tip: Oid,
+
    ) -> Result<Self, CiEventError> {
+
        Ok(Self::V1(CiEventV1::PatchCreated {
+
            from_node: node,
+
            repo,
+
            patch,
+
            new_tip: tip,
+
        }))
+
    }
+

+
    pub fn patch_updated(
+
        node: NodeId,
+
        repo: RepoId,
+
        patch: PatchId,
+
        tip: Oid,
+
    ) -> Result<Self, CiEventError> {
+
        Ok(Self::V1(CiEventV1::PatchUpdated {
+
            from_node: node,
+
            repo,
+
            patch,
+
            new_tip: tip,
+
        }))
+
    }
+

    pub fn from_node_event(event: &Event) -> Result<Vec<Self>, CiEventError> {
        fn ref_string(s: String) -> Result<RefString, CiEventError> {
            RefString::try_from(s.clone()).map_err(|e| CiEventError::ref_string(s, e))
modified src/msg.rs
@@ -31,7 +31,10 @@ use radicle::{
    Profile,
};

-
use crate::ci_event::{CiEvent, CiEventV1};
+
use crate::{
+
    ci_event::{CiEvent, CiEventV1},
+
    logger,
+
};

// This gets put into every [`Request`] message so the adapter can
// detect its getting a message it knows how to handle.
@@ -227,17 +230,62 @@ impl<'a> RequestBuilder<'a> {
                    patch: None,
                })
            }
+
            Some(CiEvent::V1(CiEventV1::BranchDeleted {
+
                from_node,
+
                repo,
+
                branch,
+
                tip,
+
            })) => {
+
                let rad_repo = profile.storage.repository(*repo)?;
+
                let project_info = rad_repo.project()?;
+

+
                let common = EventCommonFields {
+
                    version: PROTOCOL_VERSION,
+
                    event_type: EventType::Push,
+
                    repository: Repository {
+
                        id: *repo,
+
                        name: project_info.name().to_string(),
+
                        description: project_info.description().to_string(),
+
                        private: !rad_repo.identity()?.visibility().is_public(),
+
                        default_branch: project_info.default_branch().to_string(),
+
                        delegates: rad_repo.delegates()?.iter().copied().collect(),
+
                    },
+
                };
+

+
                let did = Did::from(*from_node);
+
                let pusher = did_to_author(profile, &did)?;
+

+
                let commits = vec![*tip];
+

+
                let push = PushEvent {
+
                    pusher,
+
                    before: *tip, // Branch created: we only use the tip
+
                    after: *tip,
+
                    branch: branch.as_str().to_string(),
+
                    commits,
+
                };
+

+
                Ok(Request::Trigger {
+
                    common,
+
                    push: Some(push),
+
                    patch: None,
+
                })
+
            }
            Some(CiEvent::V1(CiEventV1::PatchCreated {
                from_node,
                repo,
                patch: patch_id,
                new_tip,
            })) => {
+
                logger::debug("create Trigger from PatchCreated: open rad repository");
                let rad_repo = profile.storage.repository(*repo)?;
+
                logger::debug("open git repository");
                let git_repo =
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
+
                logger::debug("open project");
                let project_info = rad_repo.project()?;

+
                logger::debug("create common fields");
                let common = EventCommonFields {
                    version: PROTOCOL_VERSION,
                    event_type: EventType::Patch,
@@ -251,13 +299,16 @@ impl<'a> RequestBuilder<'a> {
                    },
                };

+
                logger::debug("look up did and author");
                let did = Did::from(*from_node);
                let author = did_to_author(profile, &did)?;

+
                logger::debug("look up patch COB");
                let patch_cob = patch::Patches::open(&rad_repo)?
                    .get(patch_id)?
                    .ok_or(MessageError::Trigger)?;

+
                logger::debug("look up revisions");
                let revisions: Vec<Revision> = patch_cob
                    .revisions()
                    .map(|(rid, r)| {
@@ -271,11 +322,15 @@ impl<'a> RequestBuilder<'a> {
                        })
                    })
                    .collect::<Result<Vec<Revision>, MessageError>>()?;
+
                logger::debug("look up author public key");
                let patch_author_pk = radicle::crypto::PublicKey::from(author.id);
+
                logger::debug("look up latest revision");
                let patch_latest_revision = patch_cob
                    .latest_by(&patch_author_pk)
                    .ok_or(MessageError::Trigger)?;
+
                logger::debug("look up patch base");
                let patch_base = patch_latest_revision.1.base();
+
                logger::debug("look up commits");
                let commits: Vec<Oid> = git_repo
                    .history(*new_tip)?
                    .take_while(|c| {
@@ -288,6 +343,7 @@ impl<'a> RequestBuilder<'a> {
                    .map(|r| r.map(|c| c.id))
                    .collect::<Result<Vec<Oid>, _>>()?;

+
                logger::debug("construct Patch");
                let patch = Patch {
                    id: **patch_id,
                    author,
@@ -308,6 +364,7 @@ impl<'a> RequestBuilder<'a> {
                    revisions,
                };

+
                logger::debug("construct Trigger");
                Ok(Request::Trigger {
                    common,
                    push: None,
modified src/queueproc.rs
@@ -156,7 +156,57 @@ impl QueueProcessor {
                    .map_err(QueueError::execute_ci)?;
                Ok(false)
            }
-
            _ => unimplemented!("unknown CI event {event:#?}"),
+
            CiEvent::V1(CiEventV1::BranchDeleted {
+
                from_node: _,
+
                repo,
+
                branch: _,
+
                tip,
+
            }) => {
+
                logger::queueproc_action_run(repo, tip);
+
                let trigger = RequestBuilder::default()
+
                    .profile(&self.profile)
+
                    .ci_event(event)
+
                    .build_trigger_from_ci_event()
+
                    .map_err(|e| QueueError::build_trigger(event, e))?;
+
                self.broker
+
                    .execute_ci(&trigger, &self.run_tx)
+
                    .map_err(QueueError::execute_ci)?;
+
                Ok(false)
+
            }
+
            CiEvent::V1(CiEventV1::PatchCreated {
+
                from_node: _,
+
                repo,
+
                patch: _,
+
                new_tip,
+
            }) => {
+
                logger::queueproc_action_run(repo, new_tip);
+
                let trigger = RequestBuilder::default()
+
                    .profile(&self.profile)
+
                    .ci_event(event)
+
                    .build_trigger_from_ci_event()
+
                    .map_err(|e| QueueError::build_trigger(event, e))?;
+
                self.broker
+
                    .execute_ci(&trigger, &self.run_tx)
+
                    .map_err(QueueError::execute_ci)?;
+
                Ok(false)
+
            }
+
            CiEvent::V1(CiEventV1::PatchUpdated {
+
                from_node: _,
+
                repo,
+
                patch: _,
+
                new_tip,
+
            }) => {
+
                logger::queueproc_action_run(repo, new_tip);
+
                let trigger = RequestBuilder::default()
+
                    .profile(&self.profile)
+
                    .ci_event(event)
+
                    .build_trigger_from_ci_event()
+
                    .map_err(|e| QueueError::build_trigger(event, e))?;
+
                self.broker
+
                    .execute_ci(&trigger, &self.run_tx)
+
                    .map_err(QueueError::execute_ci)?;
+
                Ok(false)
+
            }
        }
    }

modified src/util.rs
@@ -1,4 +1,7 @@
-
use std::str::FromStr;
+
use std::{
+
    path::{Path, PathBuf},
+
    str::FromStr,
+
};

use time::{
    format_description::{well_known::Rfc2822, FormatItem},
@@ -8,6 +11,7 @@ use time::{
};

use radicle::{
+
    cob::ObjectId,
    prelude::{NodeId, RepoId},
    storage::ReadStorage,
    Profile, Storage,
@@ -122,6 +126,18 @@ pub fn rfc822_timestamp(ts: OffsetDateTime) -> Result<String, UtilError> {
    Ok(ts.to_string())
}

+
pub fn read_file_as_string(filename: &Path) -> Result<String, UtilError> {
+
    String::from_utf8(
+
        std::fs::read(filename).map_err(|err| UtilError::Readfile(filename.into(), err))?,
+
    )
+
    .map_err(|err| UtilError::Utf8(filename.into(), err))
+
}
+

+
pub fn read_file_as_objectid(filename: &Path) -> Result<ObjectId, UtilError> {
+
    let s = read_file_as_string(filename)?;
+
    ObjectId::from_str(s.trim()).map_err(|err| UtilError::ReadObjectId(filename.into(), err))
+
}
+

#[derive(Debug, thiserror::Error)]
pub enum UtilError {
    #[error("failed to look up node profile")]
@@ -153,4 +169,13 @@ pub enum UtilError {

    #[error("failed to parse timestamp {0:?}")]
    TimestampParse(String),
+

+
    #[error("failed to read file {0}")]
+
    Readfile(PathBuf, #[source] std::io::Error),
+

+
    #[error("failed to convert file to UTF8: {0}")]
+
    Utf8(PathBuf, #[source] std::string::FromUtf8Error),
+

+
    #[error("failed to read object id from {0}")]
+
    ReadObjectId(PathBuf, #[source] radicle::cob::object::ParseObjectId),
}