Radish alpha
r
rad:zwTxygwuz5LDGBq255RA2CbNGrz8
Radicle CI broker
Radicle
Git
feat: events for created, updated, deleted Git tags
Lars Wirzenius committed 1 year ago
commit b783ef65d7e0718a1e777d378c4d5d3b66c71728
parent daa73ab
7 files changed +324 -17
modified ci-broker.md
@@ -908,6 +908,44 @@ sed -i "s/NODEID/$rid/g" "$yaml"
~~~


+
## Filter predicate `Tag`
+

+
_Want:_ We can allow an event that is about a specific tag.
+

+
_Why:_ We want to constrain CI to specific tags, such as for releases.
+

+
~~~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 file config.yaml from filter-tag.yaml
+

+
when I run cibtool --db ci-broker.db trigger --repo xyzzy
+
when I run ./env.sh cib --config config.yaml queued
+
when I run cibtool --db ci-broker.db run list --json
+
then stdout doesn't contain ""repo_name": "xyzzy""
+

+
when I try to run cibtool --db ci-broker.db trigger --repo xyzzy --commit v1.0
+
then command fails
+

+
when I run, in xyzzy, git tag -am "version 1.0" v1.0
+
when I run cibtool --db ci-broker.db trigger --repo xyzzy
+
when I run ./env.sh cib --config config.yaml queued
+
when I run cibtool --db ci-broker.db run list --json
+
then stdout doesn't contain ""repo_name": "xyzzy""
+
~~~
+

+
~~~{#filter-tag.yaml .file .json}
+
db: ci-broker.db
+
adapters:
+
  default:
+
    command: ./adapter.sh
+
triggers:
+
  - adapter: default
+
    filters:
+
      - !Tag "v1.0"
+
~~~
+

## Filter predicate `Branch`

_Want:_ We can allow an event that is about a specific branch.
modified src/ci_event.rs
@@ -12,6 +12,8 @@ use radicle::{
    storage::RefUpdate,
};

+
use crate::refs::ref_string;
+

#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CiEvent {
@@ -41,6 +43,25 @@ pub enum CiEventV1 {
        branch: BranchName,
        tip: Oid,
    },
+
    TagCreated {
+
        from_node: NodeId,
+
        repo: RepoId,
+
        tag: RefString,
+
        tip: Oid,
+
    },
+
    TagUpdated {
+
        from_node: NodeId,
+
        repo: RepoId,
+
        tag: RefString,
+
        tip: Oid,
+
        old_tip: Oid,
+
    },
+
    TagDeleted {
+
        from_node: NodeId,
+
        repo: RepoId,
+
        tag: RefString,
+
        tip: Oid,
+
    },
    PatchCreated {
        from_node: NodeId,
        repo: RepoId,
@@ -62,6 +83,9 @@ impl CiEvent {
            Self::V1(CiEventV1::BranchCreated { from_node, .. }) => Some(from_node),
            Self::V1(CiEventV1::BranchUpdated { from_node, .. }) => Some(from_node),
            Self::V1(CiEventV1::BranchDeleted { from_node, .. }) => Some(from_node),
+
            Self::V1(CiEventV1::TagCreated { from_node, .. }) => Some(from_node),
+
            Self::V1(CiEventV1::TagUpdated { from_node, .. }) => Some(from_node),
+
            Self::V1(CiEventV1::TagDeleted { from_node, .. }) => Some(from_node),
            Self::V1(CiEventV1::PatchCreated { from_node, .. }) => Some(from_node),
            Self::V1(CiEventV1::PatchUpdated { from_node, .. }) => Some(from_node),
        }
@@ -73,6 +97,9 @@ impl CiEvent {
            Self::V1(CiEventV1::BranchCreated { repo, .. }) => Some(repo),
            Self::V1(CiEventV1::BranchUpdated { repo, .. }) => Some(repo),
            Self::V1(CiEventV1::BranchDeleted { repo, .. }) => Some(repo),
+
            Self::V1(CiEventV1::TagCreated { repo, .. }) => Some(repo),
+
            Self::V1(CiEventV1::TagUpdated { repo, .. }) => Some(repo),
+
            Self::V1(CiEventV1::TagDeleted { repo, .. }) => Some(repo),
            Self::V1(CiEventV1::PatchCreated { repo, .. }) => Some(repo),
            Self::V1(CiEventV1::PatchUpdated { repo, .. }) => Some(repo),
        }
@@ -84,6 +111,23 @@ impl CiEvent {
            Self::V1(CiEventV1::BranchCreated { branch, .. }) => Some(branch),
            Self::V1(CiEventV1::BranchUpdated { branch, .. }) => Some(branch),
            Self::V1(CiEventV1::BranchDeleted { branch, .. }) => Some(branch),
+
            Self::V1(CiEventV1::TagCreated { .. }) => None,
+
            Self::V1(CiEventV1::TagUpdated { .. }) => None,
+
            Self::V1(CiEventV1::TagDeleted { .. }) => None,
+
            Self::V1(CiEventV1::PatchCreated { .. }) => None,
+
            Self::V1(CiEventV1::PatchUpdated { .. }) => None,
+
        }
+
    }
+

+
    pub fn tag(&self) -> Option<&RefString> {
+
        match self {
+
            Self::V1(CiEventV1::Shutdown) => None,
+
            Self::V1(CiEventV1::BranchCreated { .. }) => None,
+
            Self::V1(CiEventV1::BranchUpdated { .. }) => None,
+
            Self::V1(CiEventV1::BranchDeleted { .. }) => None,
+
            Self::V1(CiEventV1::TagCreated { tag, .. }) => Some(tag),
+
            Self::V1(CiEventV1::TagUpdated { tag, .. }) => Some(tag),
+
            Self::V1(CiEventV1::TagDeleted { tag, .. }) => Some(tag),
            Self::V1(CiEventV1::PatchCreated { .. }) => None,
            Self::V1(CiEventV1::PatchUpdated { .. }) => None,
        }
@@ -95,6 +139,9 @@ impl CiEvent {
            Self::V1(CiEventV1::BranchCreated { .. }) => None,
            Self::V1(CiEventV1::BranchUpdated { .. }) => None,
            Self::V1(CiEventV1::BranchDeleted { .. }) => None,
+
            Self::V1(CiEventV1::TagCreated { .. }) => None,
+
            Self::V1(CiEventV1::TagUpdated { .. }) => None,
+
            Self::V1(CiEventV1::TagDeleted { .. }) => None,
            Self::V1(CiEventV1::PatchCreated { patch, .. }) => Some(patch),
            Self::V1(CiEventV1::PatchUpdated { patch, .. }) => Some(patch),
        }
@@ -106,6 +153,9 @@ impl CiEvent {
            Self::V1(CiEventV1::BranchCreated { tip, .. }) => Some(tip),
            Self::V1(CiEventV1::BranchUpdated { tip, .. }) => Some(tip),
            Self::V1(CiEventV1::BranchDeleted { tip, .. }) => Some(tip),
+
            Self::V1(CiEventV1::TagCreated { tip, .. }) => Some(tip),
+
            Self::V1(CiEventV1::TagUpdated { tip, .. }) => Some(tip),
+
            Self::V1(CiEventV1::TagDeleted { tip, .. }) => Some(tip),
            Self::V1(CiEventV1::PatchCreated { new_tip, .. }) => Some(new_tip),
            Self::V1(CiEventV1::PatchUpdated { new_tip, .. }) => Some(new_tip),
        }
@@ -158,6 +208,53 @@ impl CiEvent {
        }))
    }

+
    pub fn tag_created(
+
        from_node: NodeId,
+
        repo: RepoId,
+
        tag: &RefString,
+
        tip: Oid,
+
    ) -> Result<Self, CiEventError> {
+
        assert!(!tag.starts_with("refs/"));
+
        Ok(Self::V1(CiEventV1::TagCreated {
+
            from_node,
+
            repo,
+
            tag: tag.clone(),
+
            tip,
+
        }))
+
    }
+

+
    pub fn tag_updated(
+
        from_node: NodeId,
+
        repo: RepoId,
+
        tag: &RefString,
+
        tip: Oid,
+
        old_tip: Oid,
+
    ) -> Result<Self, CiEventError> {
+
        assert!(!tag.starts_with("refs/"));
+
        Ok(Self::V1(CiEventV1::TagUpdated {
+
            from_node,
+
            repo,
+
            tag: tag.clone(),
+
            tip,
+
            old_tip,
+
        }))
+
    }
+

+
    pub fn tag_deleted(
+
        from_node: NodeId,
+
        repo: RepoId,
+
        tag: &RefString,
+
        tip: Oid,
+
    ) -> Result<Self, CiEventError> {
+
        assert!(!tag.starts_with("refs/"));
+
        Ok(Self::V1(CiEventV1::TagDeleted {
+
            from_node,
+
            repo,
+
            tag: tag.clone(),
+
            tip,
+
        }))
+
    }
+

    pub fn patch_created(from_node: NodeId, repo: RepoId, patch: PatchId, tip: Oid) -> Self {
        Self::V1(CiEventV1::PatchCreated {
            from_node,
@@ -196,7 +293,9 @@ impl CiEvent {
                                Some(ParsedRef::Patch(patch_id)) => {
                                    Self::patch_created(origin, *rid, patch_id, *oid)
                                }
-
                                Some(ParsedRef::Tag(_tag_name)) => unimplemented!(),
+
                                Some(ParsedRef::Tag(tag_name)) => {
+
                                    Self::tag_created(origin, *rid, &tag_name, *oid)?
+
                                }
                                None => continue,
                            }
                        }
@@ -209,7 +308,9 @@ impl CiEvent {
                                Some(ParsedRef::Patch(patch_id)) => {
                                    Self::patch_updated(origin, *rid, patch_id, *new)
                                }
-
                                Some(ParsedRef::Tag(_tag_name)) => unimplemented!(),
+
                                Some(ParsedRef::Tag(tag_name)) => {
+
                                    Self::tag_updated(origin, *rid, &tag_name, *new, *old)?
+
                                }
                                None => continue,
                            }
                        }
@@ -220,7 +321,9 @@ impl CiEvent {
                                    Self::branch_deleted(origin, *rid, &branch, *oid)?
                                }
                                Some(ParsedRef::Patch(_patch_id)) => continue,
-
                                Some(ParsedRef::Tag(_tag_name)) => unimplemented!(),
+
                                Some(ParsedRef::Tag(tag_name)) => {
+
                                    Self::tag_deleted(origin, *rid, &tag_name, *oid)?
+
                                }
                                None => continue,
                            }
                        }
@@ -564,7 +667,7 @@ mod test {
enum ParsedRef {
    Branch(BranchName),
    Patch(PatchId),
-
    Tag(String),
+
    Tag(RefString),
}

impl ParsedRef {
@@ -604,7 +707,9 @@ impl ParsedRef {
            let re = Regex::new(PATTERN).unwrap();
            if let Some(captures) = re.captures(refname) {
                if let Some(tag_name) = captures.get(1) {
-
                    return Some(ParsedRef::Tag(tag_name.as_str().to_string()));
+
                    if let Ok(tag_name) = ref_string(tag_name.as_str()) {
+
                        return Some(ParsedRef::Tag(tag_name));
+
                    }
                }
            }
            None
@@ -652,7 +757,7 @@ mod test_parsed_ref {
    #[test]
    fn tag() {
        let actual = ref_string("refs/namespaces/NID/refs/tags/v0.0.0").unwrap();
-
        let wanted = "v0.0.0".to_string();
+
        let wanted = ref_string("v0.0.0").unwrap();
        assert_eq!(ParsedRef::parse_ref(&actual), Some(ParsedRef::Tag(wanted)));
    }
}
modified src/filter.rs
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};

use radicle::{
    cob::patch::PatchId,
-
    git::{raw::ObjectType, BranchName, Oid},
+
    git::{raw::ObjectType, BranchName, Oid, RefString},
    node::NodeId,
    prelude::{Profile, RepoId},
    storage::git::Repository,
@@ -13,6 +13,7 @@ use radicle::{
use crate::{
    ci_event::{CiEvent, CiEventV1},
    config::TriggerConfig,
+
    logger,
    refs::ref_string,
};

@@ -54,6 +55,9 @@ pub enum EventFilter {
    /// Event is for a specific branch.
    Branch(BranchName),

+
    /// Event is for a specific tag.
+
    Tag(RefString),
+

    /// Event if for the default branch for the repository.
    DefaultBranch,

@@ -151,6 +155,11 @@ impl EventFilter {
                    format!("wanted={wanted} actual={actual:?}"),
                )
            }
+
            Self::Tag(wanted) => {
+
                let actual = event.tag();
+
                let allowed = Some(wanted) == actual;
+
                Decision::string("Tag", allowed, format!("wanted={wanted} actual={actual:?}"))
+
            }
            Self::DefaultBranch => {
                let repo = event.repository();
                let actual = event.branch();
@@ -211,6 +220,7 @@ impl EventFilter {

    pub fn allows(&self, event: &CiEvent) -> bool {
        let dec = self.decide(event);
+
        logger::queueproc_filter_decision(event, self, dec.allowed);
        dec.allowed
    }

@@ -298,6 +308,7 @@ pub fn get_default_branch(repo_id: &RepoId) -> Result<BranchName, Box<dyn std::e
    Ok(proj.default_branch().clone())
}

+
#[allow(clippy::unwrap_used)]
fn has_file(repo_id: &RepoId, oid: &Oid, filename: &Path) -> bool {
    fn helper(
        repo_id: &RepoId,
@@ -307,8 +318,24 @@ fn has_file(repo_id: &RepoId, oid: &Oid, filename: &Path) -> bool {
        let profile = Profile::load()?;
        let repo = Repository::open(profile.storage.path().join(repo_id.canonical()), *repo_id)?;

-
        let obj = repo.backend.find_object(*oid, Some(ObjectType::Commit))?;
-
        let commit = if let Ok(commit) = obj.into_commit() {
+
        let obj = repo.backend.find_object(*oid, None);
+
        let obj = obj?;
+
        let commit = match obj.kind() {
+
            None => return Ok(false),
+
            Some(ObjectType::Any) => return Ok(false),
+
            Some(ObjectType::Tree) => return Ok(false),
+
            Some(ObjectType::Blob) => return Ok(false),
+

+
            Some(ObjectType::Commit) => obj.into_commit(),
+
            Some(ObjectType::Tag) => {
+
                let tag = obj.into_tag().unwrap();
+
                repo.backend
+
                    .find_object(tag.target_id(), None)
+
                    .unwrap()
+
                    .into_commit()
+
            }
+
        };
+
        let commit = if let Ok(commit) = commit {
            commit
        } else {
            return Ok(false);
@@ -321,6 +348,7 @@ fn has_file(repo_id: &RepoId, oid: &Oid, filename: &Path) -> bool {
        };
        let obj = entry.to_object(&repo.backend)?;
        let ok = obj.into_blob().is_ok();
+

        Ok(ok)
    }

modified src/logger.rs
@@ -513,12 +513,13 @@ pub fn queueproc_remove_event(id: &QueueId) {
    );
}

-
pub fn queueproc_action_run(rid: &RepoId, oid: &Oid) {
+
pub fn queueproc_action_run(rid: &RepoId, oid: &Oid, msg: &str) {
    info!(
        msg_id = ?Id::QueueProcActionRun,
        kind = %Kind::Debug,
        ?rid,
        ?oid,
+
        msg = msg,
        "Action: run"
    );
}
modified src/msg.rs
@@ -332,6 +332,68 @@ impl<'a> RequestBuilder<'a> {
                    patch: None,
                })
            }
+
            Some(CiEvent::V1(CiEventV1::TagCreated {
+
                from_node,
+
                repo,
+
                tag,
+
                tip,
+
            })) => {
+
                Ok(Request::Trigger {
+
                    common: common_fields(EventType::Push, repo, profile)?,
+
                    push: Some(PushEvent {
+
                        pusher: author(from_node, profile)?,
+
                        before: *tip, // Branch created: we only use the tip
+
                        after: *tip,
+
                        branch: tag.as_str().to_string(),
+
                        commits: vec![*tip], // Branch created, only use tip.
+
                    }),
+
                    patch: None,
+
                })
+
            }
+
            Some(CiEvent::V1(CiEventV1::TagUpdated {
+
                from_node,
+
                repo,
+
                tag,
+
                tip,
+
                old_tip,
+
            })) => {
+
                let git_repo =
+
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
+
                let mut commits = commits(&git_repo, *tip, *old_tip)?;
+
                if commits.is_empty() {
+
                    commits = vec![*old_tip];
+
                }
+

+
                Ok(Request::Trigger {
+
                    common: common_fields(EventType::Push, repo, profile)?,
+
                    push: Some(PushEvent {
+
                        pusher: author(from_node, profile)?,
+
                        before: *tip, // Branch created: we only use the tip
+
                        after: *tip,
+
                        branch: tag.as_str().to_string(),
+
                        commits,
+
                    }),
+
                    patch: None,
+
                })
+
            }
+
            Some(CiEvent::V1(CiEventV1::TagDeleted {
+
                from_node,
+
                repo,
+
                tag,
+
                tip,
+
            })) => {
+
                Ok(Request::Trigger {
+
                    common: common_fields(EventType::Push, repo, profile)?,
+
                    push: Some(PushEvent {
+
                        pusher: author(from_node, profile)?,
+
                        before: *tip, // Branch created: we only use the tip
+
                        after: *tip,
+
                        branch: tag.as_str().to_string(),
+
                        commits: vec![*tip],
+
                    }),
+
                    patch: None,
+
                })
+
            }
            Some(CiEvent::V1(CiEventV1::PatchCreated {
                from_node,
                repo,
@@ -433,7 +495,7 @@ pub enum Request {
        #[serde(flatten)]
        common: EventCommonFields,

-
        /// The push event, if any.
+
        /// The push event, if any. `branch` may tag name if tag event.
        #[serde(flatten)]
        push: Option<PushEvent>,

@@ -543,6 +605,9 @@ pub enum EventType {

    /// A new or changed patch.
    Patch,
+

+
    /// A new or changed tag.
+
    Tag,
}

/// Common fields in all variations of a [`Request`] message.
modified src/pages.rs
@@ -255,6 +255,22 @@ impl PageData {
                CiEvent::V1(CiEventV1::BranchDeleted {
                    repo, branch, tip, ..
                }) => render_event(repo, self.repo_alias(*repo), branch, tip),
+
                CiEvent::V1(CiEventV1::TagCreated {
+
                    from_node: _,
+
                    repo,
+
                    tag,
+
                    tip,
+
                }) => render_event(repo, self.repo_alias(*repo), tag, tip),
+
                CiEvent::V1(CiEventV1::TagUpdated {
+
                    from_node: _,
+
                    repo,
+
                    tag,
+
                    tip,
+
                    old_tip: _,
+
                }) => render_event(repo, self.repo_alias(*repo), tag, tip),
+
                CiEvent::V1(CiEventV1::TagDeleted { repo, tag, tip, .. }) => {
+
                    render_event(repo, self.repo_alias(*repo), tag, tip)
+
                }
                CiEvent::V1(CiEventV1::PatchCreated {
                    from_node: _,
                    repo,
modified src/queueproc.rs
@@ -135,7 +135,7 @@ impl QueueProcessor {
                            first_error = Some(err);
                        }
                    }
-
                    Ok(None) => self.drop_event(qe.id())?,
+
                    Ok(None) => (), // We already removed event from queue.
                    Ok(Some(adapters)) => {
                        for adapter in adapters {
                            match self.run_adapter(&qe, &adapter) {
@@ -250,7 +250,7 @@ impl QueueProcessor {
                branch: _,
                tip,
            }) => {
-
                logger::queueproc_action_run(repo, tip);
+
                logger::queueproc_action_run(repo, tip, "branch created");
                let trigger = RequestBuilder::default()
                    .profile(&self.profile)
                    .ci_event(event)
@@ -268,7 +268,7 @@ impl QueueProcessor {
                tip,
                old_tip: _,
            }) => {
-
                logger::queueproc_action_run(repo, tip);
+
                logger::queueproc_action_run(repo, tip, "branch updated");
                let trigger = RequestBuilder::default()
                    .profile(&self.profile)
                    .ci_event(event)
@@ -285,7 +285,61 @@ impl QueueProcessor {
                branch: _,
                tip,
            }) => {
-
                logger::queueproc_action_run(repo, tip);
+
                logger::queueproc_action_run(repo, tip, "branch deleted");
+
                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(adapter, &trigger, &self.run_tx)
+
                    .map_err(QueueError::execute_ci)?;
+
                Ok(false)
+
            }
+
            CiEvent::V1(CiEventV1::TagCreated {
+
                from_node: _,
+
                repo,
+
                tag: _,
+
                tip,
+
            }) => {
+
                logger::queueproc_action_run(repo, tip, "tag created");
+
                let result = RequestBuilder::default()
+
                    .profile(&self.profile)
+
                    .ci_event(event)
+
                    .build_trigger_from_ci_event()
+
                    .map_err(|e| QueueError::build_trigger(event, e));
+
                logger::queueproc_trigger(&result);
+
                let trigger = result?;
+
                self.broker
+
                    .execute_ci(adapter, &trigger, &self.run_tx)
+
                    .map_err(QueueError::execute_ci)?;
+
                Ok(false)
+
            }
+
            CiEvent::V1(CiEventV1::TagUpdated {
+
                from_node: _,
+
                repo,
+
                tag: _,
+
                tip,
+
                old_tip: _,
+
            }) => {
+
                logger::queueproc_action_run(repo, tip, "tag updated");
+
                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(adapter, &trigger, &self.run_tx)
+
                    .map_err(QueueError::execute_ci)?;
+
                Ok(false)
+
            }
+
            CiEvent::V1(CiEventV1::TagDeleted {
+
                from_node: _,
+
                repo,
+
                tag: _,
+
                tip,
+
            }) => {
+
                logger::queueproc_action_run(repo, tip, "tag deleted");
                let trigger = RequestBuilder::default()
                    .profile(&self.profile)
                    .ci_event(event)
@@ -302,7 +356,7 @@ impl QueueProcessor {
                patch: _,
                new_tip,
            }) => {
-
                logger::queueproc_action_run(repo, new_tip);
+
                logger::queueproc_action_run(repo, new_tip, "patch created");
                let trigger = RequestBuilder::default()
                    .profile(&self.profile)
                    .ci_event(event)
@@ -319,7 +373,7 @@ impl QueueProcessor {
                patch: _,
                new_tip,
            }) => {
-
                logger::queueproc_action_run(repo, new_tip);
+
                logger::queueproc_action_run(repo, new_tip, "patch updated");
                let trigger = RequestBuilder::default()
                    .profile(&self.profile)
                    .ci_event(event)