feat: switch to kameo actor system (dropping actix)
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 7m32s
Rust / build (map[name:stable]) (push) Successful in 14m44s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m14s
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 7m32s
Rust / build (map[name:stable]) (push) Successful in 14m44s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m14s
This commit is contained in:
parent
d8c2e9a23f
commit
b7aa231925
80 changed files with 2361 additions and 1704 deletions
118
Cargo.lock
generated
118
Cargo.lock
generated
|
@ -2,63 +2,6 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "actix"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
|
||||
dependencies = [
|
||||
"actix-macros",
|
||||
"actix-rt",
|
||||
"actix_derive",
|
||||
"bitflags 2.6.0",
|
||||
"bytes",
|
||||
"crossbeam-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
|
||||
dependencies = [
|
||||
"actix-macros",
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix_derive"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.21.0"
|
||||
|
@ -807,6 +750,12 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
|
@ -1123,8 +1072,6 @@ dependencies = [
|
|||
name = "git-next"
|
||||
version = "0.13.11"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-rt",
|
||||
"anyhow",
|
||||
"assert2",
|
||||
"bon",
|
||||
|
@ -1139,6 +1086,7 @@ dependencies = [
|
|||
"git-next-core",
|
||||
"git-next-forge-forgejo",
|
||||
"git-next-forge-github",
|
||||
"kameo",
|
||||
"kxio",
|
||||
"lazy_static",
|
||||
"lettre",
|
||||
|
@ -1171,7 +1119,6 @@ dependencies = [
|
|||
name = "git-next-core"
|
||||
version = "0.13.11"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"assert2",
|
||||
"async-trait",
|
||||
"derive-with",
|
||||
|
@ -2747,6 +2694,36 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kameo"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62237a96597618543798a36ec723eb75c5ac301e2690243fd600be1f5eb3dd2d"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"futures",
|
||||
"itertools",
|
||||
"kameo_macros",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kameo_macros"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bbbd8e8d7b02bc67eae0dcbdb82c0a71cc7cc61734059ee3e7439a1ee1e0e85"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.8"
|
||||
|
@ -4354,6 +4331,7 @@ dependencies = [
|
|||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
|
@ -4389,6 +4367,17 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.21.0"
|
||||
|
@ -4691,6 +4680,15 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -101,9 +101,8 @@ take-until = "0.2"
|
|||
notify = "7.0"
|
||||
|
||||
# Actors
|
||||
actix = "0.13"
|
||||
actix-rt = "2.9"
|
||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||
kameo = "0.13"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
|
||||
# email
|
||||
lettre = "0.11"
|
||||
|
|
|
@ -50,8 +50,7 @@ git-conventional = { workspace = true }
|
|||
toml = { workspace = true }
|
||||
|
||||
# Actors
|
||||
actix = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
kameo = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
|
|
|
@ -655,6 +655,18 @@ stateDiagram-v2
|
|||
forge_github --> core
|
||||
```
|
||||
|
||||
## Actor Supervision Tree
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
Root
|
||||
Alerts
|
||||
FileWatcher
|
||||
Server
|
||||
Repo 1
|
||||
Repo 2
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
`git-next` is released under the [MIT License](./LICENSE).
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{info, Instrument as _};
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::alerts::{
|
||||
desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
|
||||
webhook::send_webhook, AlertsActor,
|
||||
};
|
||||
|
||||
impl Handler<NotifyUser> for AlertsActor {
|
||||
type Result = ();
|
||||
impl Message<NotifyUser> for AlertsActor {
|
||||
type Reply = ();
|
||||
|
||||
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: NotifyUser,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Some(shout) = &self.shout else {
|
||||
info!("No shout config available");
|
||||
debug!("No shout config available");
|
||||
return;
|
||||
};
|
||||
let net = self.net.clone();
|
||||
|
@ -21,21 +24,16 @@ impl Handler<NotifyUser> for AlertsActor {
|
|||
let Some(user_notification) = self.history.sendable(msg.peel()) else {
|
||||
return;
|
||||
};
|
||||
async move {
|
||||
if let Some(webhook_config) = shout.webhook() {
|
||||
send_webhook(&user_notification, webhook_config, &net).await;
|
||||
}
|
||||
if let Some(email_config) = shout.email() {
|
||||
send_email(&user_notification, email_config);
|
||||
}
|
||||
if let Some(desktop) = shout.desktop() {
|
||||
if desktop {
|
||||
send_desktop_notification(&user_notification);
|
||||
}
|
||||
if let Some(webhook_config) = shout.webhook() {
|
||||
send_webhook(&user_notification, webhook_config, &net).await;
|
||||
}
|
||||
if let Some(email_config) = shout.email() {
|
||||
send_email(&user_notification, email_config);
|
||||
}
|
||||
if let Some(desktop) = shout.desktop() {
|
||||
if desktop {
|
||||
send_desktop_notification(&user_notification);
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
use crate::alerts::{messages::UpdateShout, AlertsActor};
|
||||
|
||||
impl Handler<UpdateShout> for AlertsActor {
|
||||
type Result = ();
|
||||
impl Message<UpdateShout> for AlertsActor {
|
||||
type Reply = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: UpdateShout,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
self.shout.replace(msg.peel());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use derive_more::derive::Constructor;
|
||||
|
||||
use git_next_core::{git::UserNotification, server::Shout};
|
||||
|
||||
pub use history::History;
|
||||
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
|
||||
|
||||
use crate::{
|
||||
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start,
|
||||
default_on_actor_stop,
|
||||
};
|
||||
|
||||
mod desktop;
|
||||
mod email;
|
||||
|
@ -24,9 +28,12 @@ pub struct AlertsActor {
|
|||
history: History, // record of alerts sent recently (e.g. 24 hours)
|
||||
net: kxio::net::Net,
|
||||
}
|
||||
|
||||
impl Actor for AlertsActor {
|
||||
type Context = Context<Self>;
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
default_on_actor_start!(this, actor_ref);
|
||||
default_on_actor_panic!(this, actor_ref, err);
|
||||
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||
default_on_actor_stop!(this, actor_ref, reason);
|
||||
}
|
||||
|
||||
fn short_message(user_notification: &UserNotification) -> String {
|
||||
|
|
19
crates/cli/src/base_actor.rs
Normal file
19
crates/cli/src/base_actor.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
use kameo::{actor::ActorRef, Actor};
|
||||
|
||||
use crate::{spawn, spawn_in_thread};
|
||||
|
||||
/// Adds a spawn mathod to the actor.
|
||||
pub trait BaseActor: Actor {
|
||||
async fn spawn<Parent: Actor>(self, parent_actor_ref: ActorRef<Parent>) -> ActorRef<Self> {
|
||||
spawn!(parent_actor_ref, self)
|
||||
}
|
||||
|
||||
async fn spawn_in_thread<Parent: Actor>(
|
||||
self,
|
||||
parent_actor_ref: ActorRef<Parent>,
|
||||
) -> ActorRef<Self> {
|
||||
spawn_in_thread!(parent_actor_ref, self)
|
||||
}
|
||||
}
|
||||
impl<T: Actor> BaseActor for T {}
|
|
@ -1,50 +1,83 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use std::{path::PathBuf, sync::mpsc::Receiver};
|
||||
|
||||
use actix::Recipient;
|
||||
use anyhow::{Context, Result};
|
||||
use notify::{event::ModifyKind, Watcher};
|
||||
use anyhow::Context;
|
||||
use kameo::{mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
|
||||
use notify::{event::ModifyKind, RecommendedWatcher, Watcher};
|
||||
use tracing::{error, info};
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
use git_next_core::message;
|
||||
|
||||
use crate::{
|
||||
default_on_actor_link_died, default_on_actor_panic, default_on_actor_stop, on_actor_start,
|
||||
publish, tell, MessageBus,
|
||||
};
|
||||
|
||||
#[derive(Debug, Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct FileUpdated;
|
||||
message!(
|
||||
FileUpdated,
|
||||
"Notification that watched file has been updated"
|
||||
);
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("io")]
|
||||
Io(#[from] std::io::Error),
|
||||
message!(Watch, "Watch for the next event on the file");
|
||||
|
||||
pub struct FileWatcherActor {
|
||||
file_updates_bus: MessageBus<FileUpdated>,
|
||||
file: PathBuf,
|
||||
event_receiver: Option<Receiver<Result<notify::Event, notify::Error>>>,
|
||||
watcher: Option<RecommendedWatcher>,
|
||||
}
|
||||
pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Arc<AtomicBool>> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let shutdown = Arc::new(AtomicBool::default());
|
||||
|
||||
let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
|
||||
handler
|
||||
.watch(&path, notify::RecursiveMode::NonRecursive)
|
||||
.with_context(|| format!("Watching: {path:?}"))?;
|
||||
let thread_shutdown = shutdown.clone();
|
||||
actix_rt::task::spawn_blocking(move || {
|
||||
loop {
|
||||
if thread_shutdown.load(Ordering::Relaxed) {
|
||||
drop(handler);
|
||||
break;
|
||||
}
|
||||
for result in rx.try_iter() {
|
||||
impl FileWatcherActor {
|
||||
pub const fn new(file_updates_bus: MessageBus<FileUpdated>, file: PathBuf) -> Self {
|
||||
Self {
|
||||
file_updates_bus,
|
||||
file,
|
||||
event_receiver: None,
|
||||
watcher: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for FileWatcherActor {
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
|
||||
on_actor_start!(this, actor_ref, {
|
||||
//
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
this.event_receiver.replace(rx);
|
||||
|
||||
let mut watcher = notify::recommended_watcher(tx).context("file watcher")?;
|
||||
watcher
|
||||
.watch(&this.file, notify::RecursiveMode::NonRecursive)
|
||||
.with_context(|| format!("Watching: {:?}", this.file))?;
|
||||
this.watcher.replace(watcher);
|
||||
|
||||
tell!("file_watcher", actor_ref, Watch)?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
default_on_actor_panic!(this, actor_ref, err);
|
||||
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||
default_on_actor_stop!(this, actor_ref, reason);
|
||||
}
|
||||
|
||||
impl Message<Watch> for FileWatcherActor {
|
||||
type Reply = color_eyre::Result<()>;
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: Watch,
|
||||
ctx: kameo::message::Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
if let Some(rx) = &self.event_receiver {
|
||||
while let Ok(result) = rx.recv() {
|
||||
match result {
|
||||
Ok(event) => match event.kind {
|
||||
notify::EventKind::Modify(ModifyKind::Data(_)) => {
|
||||
info!("File modified");
|
||||
recipient.do_send(FileUpdated);
|
||||
info!("===================================================================================");
|
||||
info!(?event, "File modified");
|
||||
publish!("file_updates_bus", self.file_updates_bus, FileUpdated)?;
|
||||
break;
|
||||
}
|
||||
notify::EventKind::Modify(_)
|
||||
|
@ -55,12 +88,12 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
|
|||
| notify::EventKind::Other => { /* do nothing */ }
|
||||
},
|
||||
Err(err) => {
|
||||
error!(?err, "Watching file: {path:?}");
|
||||
error!(?err, "Watching file: {:?}", self.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
});
|
||||
Ok(shutdown)
|
||||
tell!("file_watcher", ctx.actor_ref(), msg)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
201
crates/cli/src/macros/actor.rs
Normal file
201
crates/cli/src/macros/actor.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
//
|
||||
|
||||
/// Called when the actor starts, before it processes any messages.
|
||||
///
|
||||
/// Messages sent internally by the actor during `on_start` are prioritized and processed
|
||||
/// before any externally sent messages, even if external messages are received first.
|
||||
///
|
||||
/// This ensures that the actor can properly initialize before handling external messages.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// impl Actor for ServerActor {
|
||||
/// type Mailbox = UnboundedMailbox<Self>;
|
||||
///
|
||||
/// on_actor_start!(this, actor_ref, {
|
||||
/// // handle start here
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! on_actor_start {
|
||||
($this:ident, $actor_ref:ident, $body:expr) => {
|
||||
#[allow(unused_variables)]
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn on_start(
|
||||
&mut self,
|
||||
actor_ref: kameo::actor::ActorRef<Self>,
|
||||
) -> std::result::Result<(), kameo::error::BoxError> {
|
||||
tracing::debug!(?actor_ref, "{}", <Self as Actor>::name());
|
||||
let $this = self;
|
||||
let $actor_ref = actor_ref;
|
||||
$body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! default_on_actor_start {
|
||||
($this:ident, $actor_ref:ident) => {
|
||||
$crate::on_actor_start!($this, $actor_ref, { Ok(()) });
|
||||
};
|
||||
}
|
||||
|
||||
/// Called swhen the actor encounters a panic or an error during "tell" message handling.
|
||||
///
|
||||
/// This method gives the actor an opportunity to clean up or reset its state and determine
|
||||
/// whether it should be stopped or continue processing messages.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `err`: The panic or error that occurred.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(ActorStopReason)`: Stops the actor.
|
||||
/// - `None`: Allows the actor to continue processing messages.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// impl Actor for ServerActor {
|
||||
/// type Mailbox = UnboundedMailbox<Self>;
|
||||
///
|
||||
/// on_actor_panic!(this, actor_ref, err, {
|
||||
/// // handle panic here
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! on_actor_panic {
|
||||
($this:ident, $actor_ref:ident, $err:ident, $body:expr) => {
|
||||
#[allow(unused_variables)]
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn on_panic(
|
||||
&mut self,
|
||||
actor_ref: kameo::actor::WeakActorRef<Self>,
|
||||
err: kameo::error::PanicError,
|
||||
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
|
||||
tracing::debug!(?actor_ref, %err, "{}", <Self as Actor>::name());
|
||||
let $this = self;
|
||||
let $actor_ref = actor_ref;
|
||||
let $err = err;
|
||||
$body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! default_on_actor_panic {
|
||||
($this:ident, $actor_ref:ident, $err:ident) => {
|
||||
$crate::on_actor_panic!($this, $actor_ref, $err, {
|
||||
Ok(Some(kameo::error::ActorStopReason::Panicked($err)))
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// Called when a linked actor dies.
|
||||
///
|
||||
/// By default, the actor will stop if the reason for the linked actor's death is anything other
|
||||
/// than `Normal`. You can customize this behavior in the implementation.
|
||||
///
|
||||
/// # Returns
|
||||
/// Whether the actor should stop or continue processing messages.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// impl Actor for ServerActor {
|
||||
/// type Mailbox = UnboundedMailbox<Self>;
|
||||
///
|
||||
/// on_actor_link_died!(this, actor_ref, id, reason, {
|
||||
/// // handle link death here
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! on_actor_link_died {
|
||||
($this:ident, $actor_ref:ident, $id:ident, $reason:ident, $body:expr) => {
|
||||
#[allow(unused_variables)]
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn on_link_died(
|
||||
&mut self,
|
||||
actor_ref: kameo::actor::WeakActorRef<Self>,
|
||||
id: kameo::actor::ActorID,
|
||||
reason: kameo::error::ActorStopReason,
|
||||
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
|
||||
tracing::debug!(?actor_ref, %id, %reason, "{}", <Self as Actor>::name());
|
||||
let $this = self;
|
||||
let $actor_ref = actor_ref;
|
||||
let $id = id;
|
||||
let $reason = reason;
|
||||
$body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! default_on_actor_link_died {
|
||||
($this:ident, $actor_ref:ident, $id:ident, $reason:ident) => {
|
||||
$crate::on_actor_link_died!($this, $actor_ref, $id, $reason, {
|
||||
match &$reason {
|
||||
kameo::error::ActorStopReason::Normal => Ok(None),
|
||||
kameo::error::ActorStopReason::Killed
|
||||
| kameo::error::ActorStopReason::Panicked(_)
|
||||
| kameo::error::ActorStopReason::LinkDied { .. } => {
|
||||
Ok(Some(kameo::error::ActorStopReason::LinkDied {
|
||||
id: $id,
|
||||
reason: Box::new($reason),
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// Called before the actor stops.
|
||||
///
|
||||
/// This allows the actor to perform any necessary cleanup or release resources before being fully stopped.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `reason`: The reason why the actor is being stopped.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// impl Actor for ServerActor {
|
||||
/// type Mailbox = UnboundedMailbox<Self>;
|
||||
///
|
||||
/// on_actor_stop!(this, actor_ref, id, reason, {
|
||||
/// // handle stop here
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! on_actor_stop {
|
||||
($this:ident, $actor_ref:ident, $reason:ident, $body:expr) => {
|
||||
#[allow(unused_variables)]
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn on_stop(
|
||||
&mut self,
|
||||
actor_ref: kameo::actor::WeakActorRef<Self>,
|
||||
reason: kameo::error::ActorStopReason,
|
||||
) -> std::result::Result<(), kameo::error::BoxError> {
|
||||
tracing::debug!(?actor_ref, %reason, "{}", <Self as Actor>::name());
|
||||
let $this = self;
|
||||
let $actor_ref = actor_ref;
|
||||
let $reason = reason;
|
||||
$body
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! default_on_actor_stop {
|
||||
($this:ident, $actor_ref:ident, $reason:ident) => {
|
||||
$crate::on_actor_stop!($this, $actor_ref, $reason, { Ok(()) });
|
||||
};
|
||||
}
|
3
crates/cli/src/macros/mod.rs
Normal file
3
crates/cli/src/macros/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod actor;
|
||||
mod send;
|
||||
mod spawn;
|
53
crates/cli/src/macros/send.rs
Normal file
53
crates/cli/src/macros/send.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
#[macro_export]
|
||||
macro_rules! tell {
|
||||
($actor_ref:expr, $message:expr) => {
|
||||
tell!(stringify!($actor_ref), $actor_ref, $message)
|
||||
};
|
||||
($actor_name:expr, $actor_ref:expr, $message:expr) => {{
|
||||
tracing::debug!(actor = $actor_name, msg = stringify!($message), "send");
|
||||
$actor_ref.tell($message).await
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! subscribe {
|
||||
($message_bus:expr, $actor_ref:expr) => {
|
||||
subscribe!(
|
||||
stringify!($message_bus),
|
||||
$message_bus,
|
||||
stringify!($actor_ref),
|
||||
$actor_ref
|
||||
)
|
||||
};
|
||||
($message_bus:expr, $actor_name:expr, $actor_ref:expr) => {
|
||||
subscribe!(
|
||||
stringify!($message_bus),
|
||||
$message_bus,
|
||||
$actor_name,
|
||||
$actor_ref
|
||||
)
|
||||
};
|
||||
($bus_name:expr, $message_bus:expr, $actor_ref:expr) => {
|
||||
subscribe!($bus_name, $message_bus, stringify!($actor_ref), $actor_ref)
|
||||
};
|
||||
($bus_name:expr, $message_bus:expr, $actor_name:expr, $actor_ref:expr) => {{
|
||||
tracing::debug!(msg_bus = $bus_name, actor = $actor_name, "subscribe");
|
||||
$message_bus
|
||||
.tell(kameo::actor::pubsub::Subscribe($actor_ref))
|
||||
.await
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! publish {
|
||||
($message_bus:expr, $message:expr) => {
|
||||
publish!(stringify!($message_bus), $message_bus, $message)
|
||||
};
|
||||
($bus_name:expr, $message_bus:expr, $message:expr) => {{
|
||||
tracing::debug!(bus = $bus_name, msg = stringify!($message), "publish");
|
||||
$message_bus
|
||||
.tell(kameo::actor::pubsub::Publish($message))
|
||||
.await
|
||||
}};
|
||||
}
|
25
crates/cli/src/macros/spawn.rs
Normal file
25
crates/cli/src/macros/spawn.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
/// spawns a new actor and sets up bi-directional links
|
||||
#[macro_export]
|
||||
macro_rules! spawn {
|
||||
($parent:expr, $actor:expr) => {{
|
||||
tracing::debug!("spawning : {}", stringify!($actor));
|
||||
let new_actor_ref = kameo::spawn($actor);
|
||||
new_actor_ref.link(&$parent).await;
|
||||
$parent.link(&new_actor_ref).await;
|
||||
tracing::debug!("spawned : {}", stringify!($actor));
|
||||
new_actor_ref
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! spawn_in_thread {
|
||||
($parent:expr, $actor:expr) => {{
|
||||
tracing::debug!("spawning in thread : {}", stringify!($actor));
|
||||
let new_actor_ref = kameo::actor::spawn_in_thread($actor);
|
||||
new_actor_ref.link(&$parent).await;
|
||||
$parent.link(&new_actor_ref).await;
|
||||
tracing::debug!("spawned in thread : {}", stringify!($actor));
|
||||
new_actor_ref
|
||||
}};
|
||||
}
|
|
@ -2,10 +2,13 @@
|
|||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
mod alerts;
|
||||
mod base_actor;
|
||||
mod file_watcher;
|
||||
mod forge;
|
||||
mod init;
|
||||
mod macros;
|
||||
mod repo;
|
||||
mod root;
|
||||
mod server;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
|
@ -13,9 +16,11 @@ mod tui;
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod webhook;
|
||||
|
||||
use git_next_core::git;
|
||||
use kameo::actor::{pubsub::PubSub, ActorRef};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -23,6 +28,8 @@ use clap::Parser;
|
|||
use color_eyre::Result;
|
||||
use kxio::{fs, net};
|
||||
|
||||
pub type MessageBus<T> = ActorRef<PubSub<T>>;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
|
||||
struct Commands {
|
||||
|
@ -47,6 +54,8 @@ enum Server {
|
|||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let fs = fs::new(PathBuf::default());
|
||||
let net = net::new();
|
||||
let repository_factory = git::repository::factory::real();
|
||||
|
@ -59,16 +68,16 @@ fn main() -> Result<()> {
|
|||
#[cfg(not(feature = "tui"))]
|
||||
Server::Start {} => server::start(
|
||||
false,
|
||||
fs,
|
||||
net,
|
||||
&fs,
|
||||
&net,
|
||||
repository_factory,
|
||||
std::time::Duration::from_secs(10),
|
||||
),
|
||||
#[cfg(feature = "tui")]
|
||||
Server::Start { ui } => server::start(
|
||||
ui,
|
||||
fs,
|
||||
net,
|
||||
&fs,
|
||||
&net,
|
||||
repository_factory,
|
||||
std::time::Duration::from_secs(10),
|
||||
),
|
||||
|
|
|
@ -20,7 +20,7 @@ pub fn advance_next(
|
|||
commit: Option<Commit>,
|
||||
force: git_next_core::git::push::Force,
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: RepoConfig,
|
||||
repo_config: &RepoConfig,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
message_token: MessageToken,
|
||||
) -> Result<MessageToken> {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_core::{git, RepoConfigSource};
|
||||
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
|
@ -15,45 +15,55 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<AdvanceMain> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<AdvanceMain> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
#[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.repo_details, commit = ?msg))]
|
||||
fn handle(&mut self, msg: AdvanceMain, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: AdvanceMain,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
||||
warn!("No config loaded");
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let Some(open_repository) = &self.open_repository else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let repo_details = self.repo_details.clone();
|
||||
let addr = ctx.address();
|
||||
let message_token = self.message_token;
|
||||
let commit = msg.peel();
|
||||
|
||||
self.update_tui(RepoUpdate::AdvancingMain {
|
||||
commit: commit.clone(),
|
||||
});
|
||||
|
||||
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
|
||||
})
|
||||
.await?;
|
||||
if let Err(err) = advance_main(commit, &self.repo_details, &repo_config, &**open_repository)
|
||||
{
|
||||
warn!("advance main: {err}");
|
||||
self.alert_tui(format!("advance main: {err}"));
|
||||
self.alert_tui(format!("advance main: {err}")).await?;
|
||||
} else {
|
||||
self.update_tui(RepoUpdate::MainUpdated);
|
||||
self.update_tui(RepoUpdate::MainUpdated).await?;
|
||||
if let Some(open_repository) = &self.open_repository {
|
||||
match open_repository.fetch() {
|
||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
||||
Ok(()) => {
|
||||
self.update_tui_log(git::graph::log(&self.repo_details))
|
||||
.await?;
|
||||
}
|
||||
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
|
||||
}
|
||||
}
|
||||
match repo_config.source() {
|
||||
RepoConfigSource::Repo => {
|
||||
do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
|
||||
do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
|
||||
}
|
||||
RepoConfigSource::Server => {
|
||||
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(self.message_token),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::warn;
|
||||
|
||||
use git_next_core::git;
|
||||
use tracing::{warn, Instrument};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
|
@ -14,15 +15,19 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<AdvanceNext> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<AdvanceNext> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, msg: AdvanceNext, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: AdvanceNext,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Some(repo_config) = &self.repo_details.repo_config else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let Some(open_repository) = &self.open_repository else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let AdvanceNextPayload {
|
||||
|
@ -30,45 +35,44 @@ impl Handler<AdvanceNext> for RepoActor {
|
|||
main,
|
||||
dev_commit_history,
|
||||
} = msg.peel();
|
||||
let repo_details = self.repo_details.clone();
|
||||
let repo_config = repo_config.clone();
|
||||
let addr = ctx.address();
|
||||
|
||||
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
|
||||
if let Some(commit) = &commit {
|
||||
self.update_tui(RepoUpdate::AdvancingNext {
|
||||
commit: commit.clone(),
|
||||
force: force.clone(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
};
|
||||
match advance_next(
|
||||
commit,
|
||||
force,
|
||||
&repo_details,
|
||||
&self.repo_details,
|
||||
repo_config,
|
||||
&**open_repository,
|
||||
self.message_token,
|
||||
) {
|
||||
Ok(message_token) => {
|
||||
self.update_tui(RepoUpdate::NextUpdated);
|
||||
self.update_tui(RepoUpdate::NextUpdated).await?;
|
||||
match open_repository.fetch() {
|
||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
||||
Ok(()) => {
|
||||
self.update_tui_log(git::graph::log(&self.repo_details))
|
||||
.await?;
|
||||
}
|
||||
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
|
||||
}
|
||||
// INFO: pause to allow any CI checks to be started
|
||||
let sleep_duration = self.sleep_duration;
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
tokio::time::sleep(self.sleep_duration).await;
|
||||
Ok(do_send(
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(message_token),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("advance next: {err}");
|
||||
self.alert_tui(err.to_string());
|
||||
self.alert_tui(err.to_string()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{debug, warn, Instrument as _};
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
|
@ -12,30 +12,30 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<CheckCIStatus> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<CheckCIStatus> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
||||
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
|
||||
|
||||
let addr = ctx.address();
|
||||
let forge = self.forge.duplicate();
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: CheckCIStatus,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus").await;
|
||||
let next = msg.peel();
|
||||
let log = self.log.clone();
|
||||
|
||||
self.update_tui(RepoUpdate::CheckingCI);
|
||||
self.update_tui(RepoUpdate::CheckingCI).await?;
|
||||
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
||||
async move {
|
||||
match forge.commit_status(&next).await {
|
||||
Ok(status) => {
|
||||
debug!("got status: {status:?}");
|
||||
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
|
||||
}
|
||||
Err(err) => warn!(?err, "fetching commit status"),
|
||||
match self.forge.commit_status(&next).await {
|
||||
Ok(status) => {
|
||||
debug!("got status: {status:?}");
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
ReceiveCIStatus::new((next, status)),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(err) => warn!(?err, "fetching commit status"),
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use git_next_core::git;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
|
@ -13,31 +14,38 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<CloneRepo> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<CloneRepo> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
|
||||
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||
logger(self.log.as_ref(), "Handler: CloneRepo: start");
|
||||
self.update_tui(RepoUpdate::Opening);
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: CloneRepo,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
logger(self.log.as_ref(), "Handler: CloneRepo: start").await;
|
||||
self.update_tui(RepoUpdate::Opening).await?;
|
||||
debug!("Handler: CloneRepo: start");
|
||||
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
||||
Ok(repository) => {
|
||||
logger(self.log.as_ref(), "open okay");
|
||||
logger(self.log.as_ref(), "open okay").await;
|
||||
debug!("open okay");
|
||||
self.update_tui(RepoUpdate::Opened);
|
||||
self.update_tui(RepoUpdate::Opened).await?;
|
||||
self.open_repository.replace(repository);
|
||||
if self.repo_details.repo_config.is_none() {
|
||||
do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref());
|
||||
debug!("no repo config, need to load from repo");
|
||||
do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
|
||||
} else {
|
||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
||||
debug!("have repo config");
|
||||
do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logger(self.log.as_ref(), "open failed");
|
||||
logger(self.log.as_ref(), "open failed").await;
|
||||
warn!("Could not open repo: {err:?}");
|
||||
self.alert_tui(err.to_string());
|
||||
self.alert_tui(err.to_string()).await?;
|
||||
}
|
||||
}
|
||||
debug!("Handler: CloneRepo: finish");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
use tracing::{debug, instrument, Instrument as _};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, load,
|
||||
|
@ -14,41 +14,43 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<LoadConfigFromRepo> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<LoadConfigFromRepo> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
|
||||
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: LoadConfigFromRepo,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
debug!("Handler: LoadConfigFromRepo: start");
|
||||
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
|
||||
self.update_tui(RepoUpdate::LoadingConfigFromRepo).await?;
|
||||
let Some(open_repository) = &self.open_repository else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let open_repository = open_repository.duplicate();
|
||||
let repo_details = self.repo_details.clone();
|
||||
let forge_alias = repo_details.forge.forge_alias().clone();
|
||||
let repo_alias = repo_details.repo_alias.clone();
|
||||
let addr = ctx.address();
|
||||
let notify_user_recipient = self.notify_user_recipient.clone();
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
match load::config_from_repository(repo_details, &*open_repository).await {
|
||||
Ok(repo_config) => {
|
||||
do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref());
|
||||
}
|
||||
Err(err) => notify_user(
|
||||
notify_user_recipient.as_ref(),
|
||||
match load::config_from_repository(&self.repo_details, &**open_repository).await {
|
||||
Ok(repo_config) => {
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
ReceiveRepoConfig::new(repo_config),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(err) => {
|
||||
notify_user(
|
||||
self.notify_user_recipient.as_ref(),
|
||||
UserNotification::RepoConfigLoadFailure {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||
repo_alias: self.repo_details.repo_alias.clone(),
|
||||
reason: err.to_string(),
|
||||
},
|
||||
log.as_ref(),
|
||||
),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
debug!("Handler: LoadConfigFromRepo: finish");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::debug;
|
||||
|
||||
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
|
||||
use tracing::{debug, Instrument};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
|
@ -13,64 +14,64 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<ReceiveCIStatus> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<ReceiveCIStatus> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
||||
logger(self.log.as_ref(), "start: ReceiveCIStatus");
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ReceiveCIStatus,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
logger(self.log.as_ref(), "start: ReceiveCIStatus").await;
|
||||
let (next, status) = msg.peel();
|
||||
self.update_tui(RepoUpdate::ReceiveCIStatus {
|
||||
status: status.clone(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
debug!(?status, "");
|
||||
let graph_log = graph::log(&self.repo_details);
|
||||
self.update_tui_log(graph_log.clone());
|
||||
|
||||
let addr = ctx.address();
|
||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
||||
let repo_alias = self.repo_details.repo_alias.clone();
|
||||
let message_token = self.message_token;
|
||||
let sleep_duration = self.sleep_duration;
|
||||
self.update_tui_log(graph_log.clone()).await?;
|
||||
match status {
|
||||
Status::Pass => {
|
||||
do_send(&addr, AdvanceMain::new(next), self.log.as_ref());
|
||||
do_send(&ctx.actor_ref(), AdvanceMain::new(next), self.log.as_ref()).await?;
|
||||
}
|
||||
Status::Pending => {
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
tokio::time::sleep(self.sleep_duration).await;
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(self.message_token),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Status::Fail => {
|
||||
tracing::warn!("Checks have failed");
|
||||
|
||||
notify_user(
|
||||
self.notify_user_recipient.as_ref(),
|
||||
UserNotification::CICheckFailed {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||
repo_alias: self.repo_details.repo_alias.clone(),
|
||||
commit: next,
|
||||
log: graph_log,
|
||||
},
|
||||
self.log.as_ref(),
|
||||
);
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
debug!("sleeping before retrying...");
|
||||
logger(log.as_ref(), "before sleep");
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
logger(log.as_ref(), "after sleep");
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
)
|
||||
.await?;
|
||||
debug!("sleeping before retrying...");
|
||||
logger(self.log.clone().as_ref(), "before sleep").await;
|
||||
tokio::time::sleep(self.sleep_duration).await;
|
||||
logger(self.log.clone().as_ref(), "after sleep").await;
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(self.message_token),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Status::Error(err) => {
|
||||
tracing::warn!(?err, "Check CI Status");
|
||||
}
|
||||
Status::Error(_) => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
|
@ -11,16 +12,22 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<ReceiveRepoConfig> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<ReceiveRepoConfig> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
||||
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ReceiveRepoConfig,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let repo_config = msg.peel();
|
||||
self.update_tui(RepoUpdate::ReceiveRepoConfig {
|
||||
repo_config: repo_config.clone(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
self.repo_details.repo_config.replace(repo_config);
|
||||
self.update_tui_branches();
|
||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
||||
self.update_tui_branches().await?;
|
||||
do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{debug, error, Instrument as _};
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
|
@ -14,51 +14,52 @@ use crate::{
|
|||
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
impl Handler<RegisterWebhook> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<RegisterWebhook> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, _msg: RegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: RegisterWebhook,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
if self.webhook_id.is_none() {
|
||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
||||
let repo_alias = self.repo_details.repo_alias.clone();
|
||||
let repo_listen_url = self
|
||||
.listen_url
|
||||
.repo_url(forge_alias.clone(), repo_alias.clone());
|
||||
let forge = self.forge.duplicate();
|
||||
let addr = ctx.address();
|
||||
let notify_user_recipient = self.notify_user_recipient.clone();
|
||||
let log = self.log.clone();
|
||||
self.update_tui(RepoUpdate::RegisteringWebhook);
|
||||
self.update_tui(RepoUpdate::RegisteringWebhook).await?;
|
||||
debug!("registering webhook");
|
||||
async move {
|
||||
match forge.register_webhook(&repo_listen_url).await {
|
||||
Ok(registered_webhook) => {
|
||||
debug!(?registered_webhook, "webhook registered");
|
||||
do_send(
|
||||
&addr,
|
||||
WebhookRegistered::from(registered_webhook),
|
||||
log.as_ref(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "failed to register webhook");
|
||||
notify_user(
|
||||
notify_user_recipient.as_ref(),
|
||||
UserNotification::WebhookRegistration {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
reason: err.to_string(),
|
||||
},
|
||||
log.as_ref(),
|
||||
);
|
||||
}
|
||||
match self
|
||||
.forge
|
||||
.register_webhook(&self.listen_url.repo_url(
|
||||
self.repo_details.forge.forge_alias().clone(),
|
||||
self.repo_details.repo_alias.clone(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(registered_webhook) => {
|
||||
debug!(?registered_webhook, "webhook registered");
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
WebhookRegistered::from(registered_webhook),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "failed to register webhook");
|
||||
notify_user(
|
||||
self.notify_user_recipient.clone().as_ref(),
|
||||
UserNotification::WebhookRegistration {
|
||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||
repo_alias: self.repo_details.repo_alias.clone(),
|
||||
reason: err.to_string(),
|
||||
},
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
} else {
|
||||
self.alert_tui("already have a webhook id - cant register webhook");
|
||||
self.alert_tui("already have a webhook id - cant register webhook")
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{debug, warn, Instrument as _};
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
repo::{messages::UnRegisterWebhook, RepoActor},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<UnRegisterWebhook> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<UnRegisterWebhook> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: UnRegisterWebhook,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Some(webhook_id) = self.webhook_id.take() else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
self.update_tui(RepoUpdate::UnregisteringWebhook);
|
||||
let forge = self.forge.duplicate();
|
||||
self.update_tui(RepoUpdate::UnregisteringWebhook).await?;
|
||||
debug!("unregistering webhook");
|
||||
async move {
|
||||
match forge.unregister_webhook(&webhook_id).await {
|
||||
Ok(()) => debug!("unregistered webhook"),
|
||||
Err(err) => warn!(?err, "unregistering webhook"),
|
||||
}
|
||||
match self.forge.unregister_webhook(&webhook_id).await {
|
||||
Ok(()) => debug!("unregistered webhook"),
|
||||
Err(err) => warn!(?err, "unregistering webhook"),
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
debug!("unregistering webhook done");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tracing::{info, instrument, Instrument as _};
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
|
@ -11,20 +13,23 @@ use crate::{
|
|||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
use git_next_core::git::{
|
||||
push::Force,
|
||||
validation::positions::{validate, Error, Positions},
|
||||
UserNotification,
|
||||
};
|
||||
use git_next_core::s;
|
||||
|
||||
impl Handler<ValidateRepo> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<ValidateRepo> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))]
|
||||
fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||
logger(self.log.as_ref(), "start: ValidateRepo");
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ValidateRepo,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
logger(self.log.as_ref(), "start: ValidateRepo").await;
|
||||
// Message Token - make sure we are only triggered for the latest/current token
|
||||
match self.token_status(msg.peel()) {
|
||||
TokenStatus::Current => {} // do nothing
|
||||
|
@ -32,38 +37,38 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
logger(
|
||||
self.log.as_ref(),
|
||||
format!("discarded: old message token: {}", self.message_token),
|
||||
);
|
||||
return; // message is expired
|
||||
)
|
||||
.await;
|
||||
return Ok(()); // message is expired
|
||||
}
|
||||
TokenStatus::New(message_token) => {
|
||||
self.message_token = message_token;
|
||||
logger(
|
||||
self.log.as_ref(),
|
||||
format!("new message token: {}", self.message_token),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
logger(
|
||||
self.log.as_ref(),
|
||||
format!("accepted token: {}", self.message_token),
|
||||
);
|
||||
|
||||
self.update_tui(RepoUpdate::ValidateRepo);
|
||||
|
||||
)
|
||||
.await;
|
||||
self.update_tui(RepoUpdate::ValidateRepo).await?;
|
||||
// Repository positions
|
||||
let Some(ref open_repository) = self.open_repository else {
|
||||
logger(self.log.as_ref(), "no open repository");
|
||||
self.alert_tui("repo not open");
|
||||
return;
|
||||
logger(self.log.as_ref(), "no open repository").await;
|
||||
self.alert_tui("repo not open").await?;
|
||||
return Ok(());
|
||||
};
|
||||
logger(self.log.as_ref(), "have open repository");
|
||||
logger(self.log.as_ref(), "have open repository").await;
|
||||
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
||||
logger(self.log.as_ref(), "no repo config");
|
||||
self.alert_tui("no repo config");
|
||||
return;
|
||||
logger(self.log.as_ref(), "no repo config").await;
|
||||
self.alert_tui("no repo config").await?;
|
||||
return Ok(());
|
||||
};
|
||||
logger(self.log.as_ref(), "have repo config");
|
||||
|
||||
logger(self.log.as_ref(), "have repo config").await;
|
||||
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
||||
Ok((
|
||||
Positions {
|
||||
|
@ -75,70 +80,88 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
},
|
||||
git_log,
|
||||
)) => {
|
||||
info!(%main, %next, %dev, "positions");
|
||||
self.update_tui_log(git_log);
|
||||
let mut positions = HashMap::new();
|
||||
positions
|
||||
.entry(s!(s!(main.sha())[0..7]))
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(["main"]);
|
||||
positions
|
||||
.entry(s!(s!(next.sha())[0..7]))
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(["next"]);
|
||||
positions
|
||||
.entry(s!(s!(dev.sha())[0..7]))
|
||||
.or_insert_with(Vec::new)
|
||||
.extend(["dev"]);
|
||||
info!(?positions, "");
|
||||
self.update_tui_log(git_log).await?;
|
||||
if next_is_valid && next != main {
|
||||
info!("Checking CI");
|
||||
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
CheckCIStatus::new(next),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
} else if next != dev {
|
||||
info!("Advance next");
|
||||
self.update_tui(RepoUpdate::AdvancingNext {
|
||||
commit: next.clone(),
|
||||
force: Force::No,
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
do_send(
|
||||
&ctx.address(),
|
||||
&ctx.actor_ref(),
|
||||
AdvanceNext::new(AdvanceNextPayload {
|
||||
next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
}),
|
||||
self.log.as_ref(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!("do nothing");
|
||||
self.update_tui(RepoUpdate::Okay { main, next, dev });
|
||||
self.update_tui(RepoUpdate::Okay { main, next, dev })
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(Error::Retryable(message)) => {
|
||||
info!(?message, "Retryable");
|
||||
self.alert_tui(format!("retryable: {message}"));
|
||||
logger(self.log.as_ref(), message);
|
||||
let addr = ctx.address();
|
||||
let message_token = self.message_token;
|
||||
let sleep_duration = self.sleep_duration;
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
info!("sleeping before retrying...");
|
||||
logger(log.as_ref(), "before sleep");
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
logger(log.as_ref(), "after sleep");
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
warn!(?message, "Retryable");
|
||||
self.alert_tui(format!("retryable: {message}")).await?;
|
||||
logger(self.log.as_ref(), message).await;
|
||||
debug!("sleeping before retrying...");
|
||||
tokio::time::sleep(self.sleep_duration).await;
|
||||
do_send(
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(self.message_token),
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(Error::UserIntervention(user_notification)) => {
|
||||
info!(?user_notification, "User Intervention");
|
||||
self.alert_tui(format!("USER INTERVENTION: {user_notification}"));
|
||||
warn!(?user_notification, "User Intervention");
|
||||
self.alert_tui(format!("USER INTERVENTION: {user_notification}"))
|
||||
.await?;
|
||||
if let UserNotification::CICheckFailed { log, .. }
|
||||
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
|
||||
{
|
||||
self.update_tui_log(log.clone());
|
||||
self.update_tui_log(log.clone()).await?;
|
||||
}
|
||||
notify_user(
|
||||
self.notify_user_recipient.as_ref(),
|
||||
user_notification,
|
||||
self.log.as_ref(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(Error::NonRetryable(message)) => {
|
||||
info!(?message, "NonRetryable");
|
||||
self.alert_tui(format!("Error: {message}"));
|
||||
logger(self.log.as_ref(), message);
|
||||
warn!(?message, "NonRetryable");
|
||||
self.alert_tui(format!("Error: {message}")).await?;
|
||||
logger(self.log.as_ref(), message).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use git_next_core::{
|
||||
git::{Commit, ForgeLike},
|
||||
webhook::{push::Branch, Push},
|
||||
BranchName, WebhookAuth,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, logger,
|
||||
|
@ -12,21 +18,19 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
use git_next_core::{
|
||||
git::{Commit, ForgeLike},
|
||||
webhook::{push::Branch, Push},
|
||||
BranchName, WebhookAuth,
|
||||
};
|
||||
|
||||
impl Handler<WebhookNotification> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<WebhookNotification> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
#[instrument(name = "RepoActor::WebhookMessage", skip_all, fields(token = %self.message_token, repo = %self.repo_details))]
|
||||
fn handle(&mut self, msg: WebhookNotification, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: WebhookNotification,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Some(config) = &self.repo_details.repo_config else {
|
||||
logger(self.log.as_ref(), "server has no repo config");
|
||||
logger(self.log.as_ref(), "server has no repo config").await;
|
||||
warn!("No repo config");
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
if validate_notification(
|
||||
&msg,
|
||||
|
@ -34,72 +38,78 @@ impl Handler<WebhookNotification> for RepoActor {
|
|||
&*self.forge,
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
let body = msg.body();
|
||||
match self.forge.parse_webhook_body(body) {
|
||||
match self.forge.parse_webhook_body(msg.body()) {
|
||||
Err(err) => {
|
||||
logger(self.log.as_ref(), "message parse error - not a push");
|
||||
logger(self.log.as_ref(), "message parse error - not a push").await;
|
||||
warn!(?err, "Not a 'push'");
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
Ok(push) => match push.branch(config.branches()) {
|
||||
None => {
|
||||
logger(self.log.as_ref(), "unknown branch");
|
||||
logger(self.log.as_ref(), "unknown branch").await;
|
||||
warn!(
|
||||
?push,
|
||||
"Unrecognised branch, we should be filtering to only the ones we want"
|
||||
);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
Some(Branch::Main) => {
|
||||
self.update_tui(RepoUpdate::WebhookReceived {
|
||||
branch: Branch::Main,
|
||||
push: push.clone(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
if handle_push(
|
||||
push,
|
||||
&config.branches().main(),
|
||||
&mut self.last_main_commit,
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
Some(Branch::Next) => {
|
||||
self.update_tui(RepoUpdate::WebhookReceived {
|
||||
branch: Branch::Next,
|
||||
push: push.clone(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
if handle_push(
|
||||
push,
|
||||
&config.branches().next(),
|
||||
&mut self.last_next_commit,
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
Some(Branch::Dev) => {
|
||||
self.update_tui(RepoUpdate::WebhookReceived {
|
||||
branch: Branch::Dev,
|
||||
push: push.clone(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
if handle_push(
|
||||
push,
|
||||
&config.branches().dev(),
|
||||
&mut self.last_dev_commit,
|
||||
self.log.as_ref(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -110,27 +120,29 @@ impl Handler<WebhookNotification> for RepoActor {
|
|||
"New commit"
|
||||
);
|
||||
do_send(
|
||||
&ctx.address(),
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(message_token),
|
||||
self.log.as_ref(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_notification(
|
||||
async fn validate_notification(
|
||||
msg: &WebhookNotification,
|
||||
webhook_auth: Option<&WebhookAuth>,
|
||||
forge: &dyn ForgeLike,
|
||||
log: Option<&ActorLog>,
|
||||
) -> Result<(), ()> {
|
||||
let Some(expected_authorization) = webhook_auth else {
|
||||
logger(log, "server has no auth token");
|
||||
logger(log, "server has no auth token").await;
|
||||
warn!("Don't know what authorization to expect");
|
||||
return Err(());
|
||||
};
|
||||
|
||||
if !forge.is_message_authorised(msg, expected_authorization) {
|
||||
logger(log, "message authorisation is invalid");
|
||||
logger(log, "message authorisation is invalid").await;
|
||||
warn!(
|
||||
"Invalid authorization - expected {}",
|
||||
expected_authorization
|
||||
|
@ -138,22 +150,22 @@ fn validate_notification(
|
|||
return Err(());
|
||||
}
|
||||
if forge.should_ignore_message(msg) {
|
||||
logger(log, "forge sent ignorable message");
|
||||
logger(log, "forge sent ignorable message").await;
|
||||
return Err(());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_push(
|
||||
async fn handle_push(
|
||||
push: Push,
|
||||
branch: &BranchName,
|
||||
last_commit: &mut Option<Commit>,
|
||||
log: Option<&ActorLog>,
|
||||
) -> Result<(), ()> {
|
||||
logger(log, format!("message is for {branch} branch"));
|
||||
logger(log, format!("message is for {branch} branch")).await;
|
||||
let commit = Commit::from(push);
|
||||
if last_commit.as_ref() == Some(&commit) {
|
||||
logger(log, format!("not a new commit on {branch}"));
|
||||
logger(log, format!("not a new commit on {branch}")).await;
|
||||
info!(
|
||||
%branch ,
|
||||
%commit,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
|
@ -11,17 +12,24 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<WebhookRegistered> for RepoActor {
|
||||
type Result = ();
|
||||
impl Message<WebhookRegistered> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
|
||||
fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result {
|
||||
self.update_tui(RepoUpdate::RegisteredWebhook);
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: WebhookRegistered,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
self.update_tui(RepoUpdate::RegisteredWebhook).await?;
|
||||
self.webhook_id.replace(msg.webhook_id().clone());
|
||||
self.webhook_auth.replace(msg.webhook_auth().clone());
|
||||
do_send(
|
||||
&ctx.address(),
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(self.message_token),
|
||||
self.log.as_ref(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use tracing::{info, instrument};
|
|||
/// Loads the [RepoConfig] from the `.git-next.toml` file in the repository
|
||||
#[instrument(skip_all, fields(branch = %repo_details.branch))]
|
||||
pub async fn config_from_repository(
|
||||
repo_details: RepoDetails,
|
||||
repo_details: &RepoDetails,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
) -> Result<RepoConfig> {
|
||||
info!("Loading .git-next.toml from repo");
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
alerts::messages::NotifyUser,
|
||||
server::{actor::messages::RepoUpdate, ServerActor},
|
||||
};
|
||||
use color_eyre::{eyre::eyre, Result};
|
||||
use derive_more::Deref;
|
||||
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, Actor};
|
||||
use kxio::net::Net;
|
||||
use tracing::{info, instrument, warn, Instrument};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use git_next_core::{
|
||||
git::{
|
||||
|
@ -19,6 +18,16 @@ use git_next_core::{
|
|||
WebhookAuth, WebhookId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
alerts::{messages::NotifyUser, AlertsActor},
|
||||
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start, on_actor_stop,
|
||||
server::{
|
||||
actor::messages::{RepoUpdate, ServerUpdate},
|
||||
ServerActor,
|
||||
},
|
||||
tell,
|
||||
};
|
||||
|
||||
mod branch;
|
||||
pub mod handlers;
|
||||
mod load;
|
||||
|
@ -29,14 +38,21 @@ mod notifications;
|
|||
pub mod tests;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
|
||||
pub struct ActorLog(Arc<RwLock<Vec<String>>>);
|
||||
impl Deref for ActorLog {
|
||||
type Target = std::sync::Arc<std::sync::RwLock<Vec<String>>>;
|
||||
type Target = Arc<RwLock<Vec<String>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl ActorLog {
|
||||
pub async fn log(&self, message: impl Into<String> + Send) {
|
||||
let message = message.into();
|
||||
debug!(%message, "log");
|
||||
self.write().await.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// An actor that represents a Git Repository.
|
||||
///
|
||||
|
@ -45,6 +61,7 @@ impl Deref for ActorLog {
|
|||
#[derive(Debug, derive_more::Display, derive_with::With)]
|
||||
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
|
||||
pub struct RepoActor {
|
||||
ui: bool,
|
||||
sleep_duration: std::time::Duration,
|
||||
generation: git::Generation,
|
||||
message_token: messages::MessageToken,
|
||||
|
@ -60,12 +77,13 @@ pub struct RepoActor {
|
|||
net: Net,
|
||||
forge: Box<dyn git::ForgeLike>,
|
||||
log: Option<ActorLog>,
|
||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||
server_addr: Option<Addr<ServerActor>>,
|
||||
notify_user_recipient: Option<ActorRef<AlertsActor>>,
|
||||
server_actor_ref: ActorRef<ServerActor>,
|
||||
}
|
||||
impl RepoActor {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
ui: bool,
|
||||
repo_details: git::RepoDetails,
|
||||
forge: Box<dyn git::ForgeLike>,
|
||||
listen_url: ListenUrl,
|
||||
|
@ -73,11 +91,12 @@ impl RepoActor {
|
|||
net: Net,
|
||||
repository_factory: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||
server_addr: Option<Addr<ServerActor>>,
|
||||
notify_user_recipient: Option<ActorRef<AlertsActor>>,
|
||||
server_actor_ref: ActorRef<ServerActor>,
|
||||
) -> Self {
|
||||
let message_token = messages::MessageToken::default();
|
||||
Self {
|
||||
ui,
|
||||
generation,
|
||||
message_token,
|
||||
repo_details,
|
||||
|
@ -94,111 +113,114 @@ impl RepoActor {
|
|||
sleep_duration,
|
||||
log: None,
|
||||
notify_user_recipient,
|
||||
server_addr,
|
||||
server_actor_ref,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_tui_branches(&self) {
|
||||
async fn update_tui_branches(&self) -> Result<()> {
|
||||
if cfg!(feature = "tui") {
|
||||
use crate::server::actor::messages::RepoUpdate;
|
||||
let Some(repo_config) = &self.repo_details.repo_config else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let branches = repo_config.branches().clone();
|
||||
self.update_tui(RepoUpdate::Branches { branches });
|
||||
self.update_tui(RepoUpdate::Branches { branches }).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn update_tui_log(&self, log: git::graph::Log) {
|
||||
async fn update_tui_log(&self, log: git::graph::Log) -> Result<()> {
|
||||
if cfg!(feature = "tui") {
|
||||
self.update_tui(RepoUpdate::Log { log });
|
||||
self.update_tui(RepoUpdate::Log { log }).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn alert_tui(&self, alert: impl Into<String>) {
|
||||
async fn alert_tui(&self, alert: impl Into<String> + Send) -> Result<()> {
|
||||
if cfg!(feature = "tui") {
|
||||
self.update_tui(RepoUpdate::Alert {
|
||||
alert: alert.into(),
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn update_tui(&self, repo_update: RepoUpdate) {
|
||||
if cfg!(feature = "tui") {
|
||||
let Some(server_addr) = &self.server_addr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let update = crate::server::actor::messages::ServerUpdate::RepoUpdate {
|
||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||
repo_alias: self.repo_details.repo_alias.clone(),
|
||||
repo_update,
|
||||
};
|
||||
server_addr.do_send(update);
|
||||
#[instrument(skip_all)]
|
||||
async fn update_tui(&self, repo_update: RepoUpdate) -> Result<()> {
|
||||
if cfg!(feature = "tui") && self.ui {
|
||||
tell!(
|
||||
"server",
|
||||
self.server_actor_ref,
|
||||
ServerUpdate::RepoUpdate {
|
||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||
repo_alias: self.repo_details.repo_alias.clone(),
|
||||
repo_update,
|
||||
}
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Actor for RepoActor {
|
||||
type Context = Context<Self>;
|
||||
#[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||
tracing::debug!("stopping");
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
|
||||
default_on_actor_start!(this, actor_ref);
|
||||
default_on_actor_panic!(this, actor_ref, err);
|
||||
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||
|
||||
on_actor_stop!(this, actor_ref, reason, {
|
||||
info!("Checking webhook");
|
||||
match self.webhook_id.take() {
|
||||
Some(webhook_id) => {
|
||||
tracing::warn!("stopping - unregistering webhook");
|
||||
info!(%webhook_id, "Unregistring webhook");
|
||||
let forge = self.forge.duplicate();
|
||||
async move {
|
||||
if let Err(err) = forge.unregister_webhook(&webhook_id).await {
|
||||
warn!("unregistering webhook: {err}");
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
Running::Continue
|
||||
if let Some(webhook_id) = this.webhook_id.take() {
|
||||
tracing::warn!("stopping - unregistering webhook");
|
||||
info!(%webhook_id, "Unregistring webhook");
|
||||
let forge = this.forge.duplicate();
|
||||
if let Err(err) = forge.unregister_webhook(&webhook_id).await {
|
||||
warn!("unregistering webhook: {err}");
|
||||
}
|
||||
None => Running::Stop,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>)
|
||||
pub async fn do_send<M>(
|
||||
repo_actor_ref: &ActorRef<RepoActor>,
|
||||
msg: M,
|
||||
log: Option<&ActorLog>,
|
||||
) -> Result<()>
|
||||
where
|
||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||
RepoActor: actix::Handler<M>,
|
||||
<M as actix::Message>::Result: Send,
|
||||
M: Send + Sync + 'static + std::fmt::Debug,
|
||||
RepoActor: kameo::message::Message<M>,
|
||||
<RepoActor as kameo::message::Message<M>>::Reply: Send + Sync + 'static + std::fmt::Debug,
|
||||
<<RepoActor as kameo::message::Message<M>>::Reply as kameo::Reply>::Error: std::fmt::Debug,
|
||||
{
|
||||
let log_message = format!("send: {msg:?}");
|
||||
info!(log_message);
|
||||
logger(log, log_message);
|
||||
logger(log, log_message.clone()).await;
|
||||
if cfg!(not(test)) {
|
||||
// #[cfg(not(test))]
|
||||
addr.do_send(msg);
|
||||
tell!(repo_actor_ref, msg).map_err(|e| eyre!(format!("error: {log_message}: {e:?}")))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn logger(log: Option<&ActorLog>, message: impl Into<String>) {
|
||||
pub async fn logger(log: Option<&ActorLog>, message: impl Into<String> + Send) {
|
||||
if let Some(log) = log {
|
||||
let message: String = message.into();
|
||||
tracing::debug!(message);
|
||||
let _ = log.write().map(|mut l| l.push(message));
|
||||
log.log(message).await;
|
||||
}
|
||||
}
|
||||
pub fn notify_user(
|
||||
recipient: Option<&Recipient<NotifyUser>>,
|
||||
pub async fn notify_user(
|
||||
recipient: Option<&ActorRef<AlertsActor>>,
|
||||
user_notification: UserNotification,
|
||||
log: Option<&ActorLog>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let msg = NotifyUser::from(user_notification);
|
||||
let log_message = format!("send: {msg:?}");
|
||||
tracing::debug!(log_message);
|
||||
logger(log, log_message);
|
||||
debug!(log_message);
|
||||
logger(log, log_message).await;
|
||||
if let Some(recipient) = &recipient {
|
||||
recipient.do_send(msg);
|
||||
tell!("alerts", recipient, msg)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ fn advance_next_sut(
|
|||
main: &Commit,
|
||||
dev_commit_history: &[Commit],
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: RepoConfig,
|
||||
repo_config: &RepoConfig,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
message_token: MessageToken,
|
||||
) -> branch::Result<MessageToken> {
|
||||
|
@ -43,7 +43,7 @@ mod when_at_dev {
|
|||
main,
|
||||
dev_commit_history,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
)
|
||||
|
@ -78,7 +78,7 @@ mod can_advance {
|
|||
main,
|
||||
dev_commit_history,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
)
|
||||
|
@ -109,7 +109,7 @@ mod can_advance {
|
|||
main,
|
||||
dev_commit_history,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
)
|
||||
|
@ -149,7 +149,7 @@ mod can_advance {
|
|||
main,
|
||||
dev_commit_history,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
)
|
||||
|
@ -181,7 +181,7 @@ mod can_advance {
|
|||
main,
|
||||
dev_commit_history,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ mod advance_next;
|
|||
use crate::git;
|
||||
use crate::repo::branch;
|
||||
|
||||
#[actix_rt::test]
|
||||
#[tokio::test]
|
||||
async fn test_find_next_commit_on_dev_when_next_is_at_main() {
|
||||
let next = given::a_commit(); // and main
|
||||
let expected = given::a_commit();
|
||||
|
@ -27,7 +27,7 @@ async fn test_find_next_commit_on_dev_when_next_is_at_main() {
|
|||
assert_eq!(force, Force::No, "should not try to force");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
#[tokio::test]
|
||||
async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() {
|
||||
let next = given::a_commit();
|
||||
let main = given::a_commit();
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
use crate::{
|
||||
alerts::{AlertsActor, History},
|
||||
server::{actor::messages::ServerUpdate, ServerActor},
|
||||
};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
use git_next_core::server::ListenUrl;
|
||||
use kameo::actor::{pubsub::PubSub, ActorRef};
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
|
||||
pub fn has_all_valid_remote_defaults(
|
||||
open_repository: &mut MockOpenRepositoryLike,
|
||||
|
@ -66,7 +73,7 @@ pub fn a_name() -> String {
|
|||
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
|
||||
iter::repeat_with(one_char).take(len).collect()
|
||||
}
|
||||
generate(5)
|
||||
generate(7)
|
||||
}
|
||||
|
||||
pub fn maybe_a_number() -> Option<u32> {
|
||||
|
@ -197,6 +204,7 @@ pub fn a_repo_actor(
|
|||
repo_details: RepoDetails,
|
||||
repository_factory: Box<dyn RepositoryFactory>,
|
||||
forge: Box<dyn ForgeLike>,
|
||||
server_actor: ActorRef<ServerActor>,
|
||||
net: kxio::net::Net,
|
||||
) -> (RepoActor, ActorLog) {
|
||||
let listen_url = given::a_listen_url();
|
||||
|
@ -205,21 +213,43 @@ pub fn a_repo_actor(
|
|||
let actors_log = log.clone();
|
||||
(
|
||||
RepoActor::new(
|
||||
false,
|
||||
repo_details,
|
||||
forge,
|
||||
listen_url,
|
||||
generation,
|
||||
net,
|
||||
repository_factory,
|
||||
std::time::Duration::from_nanos(1),
|
||||
None,
|
||||
std::time::Duration::from_nanos(0),
|
||||
None,
|
||||
server_actor,
|
||||
)
|
||||
.with_log(actors_log),
|
||||
log,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn a_server_actor(fs: FileSystem, net: Net) -> ActorRef<ServerActor> {
|
||||
let alerts_actor_ref = kameo::spawn(AlertsActor::new(
|
||||
None,
|
||||
History::new(Duration::from_secs(0)),
|
||||
net.clone(),
|
||||
));
|
||||
let server_updates_actor_ref = kameo::spawn(PubSub::<ServerUpdate>::new());
|
||||
let repo_factor = Box::new(MockRepositoryFactory::new());
|
||||
let duration = Duration::from_secs(0);
|
||||
let server_actor = ServerActor::new(
|
||||
false, // ui
|
||||
fs,
|
||||
net,
|
||||
alerts_actor_ref,
|
||||
server_updates_actor_ref,
|
||||
repo_factor,
|
||||
duration,
|
||||
);
|
||||
kameo::spawn(server_actor)
|
||||
}
|
||||
|
||||
pub fn a_hostname() -> Hostname {
|
||||
Hostname::new(given::a_name())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::{repo::messages::AdvanceMain, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -37,21 +39,16 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone()))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, AdvanceMain::new(next_commit.clone()))?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: LoadConfigFromRepo")));
|
||||
})?;
|
||||
log.require_message_containing("send: LoadConfigFromRepo")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -87,12 +84,10 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone()))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, AdvanceMain::new(next_commit.clone()))?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
tracing::debug!(?log, "log");
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::repo::messages::AdvanceNextPayload;
|
||||
use crate::{
|
||||
repo::messages::{AdvanceNext, AdvanceNextPayload},
|
||||
tell,
|
||||
};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -40,19 +43,18 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::AdvanceNext::new(
|
||||
AdvanceNextPayload {
|
||||
tell!(
|
||||
addr,
|
||||
AdvanceNext::new(AdvanceNextPayload {
|
||||
next: next_commit.clone(),
|
||||
main: next_commit.clone(),
|
||||
dev_commit_history,
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
||||
System::current().stop();
|
||||
})
|
||||
)?;
|
||||
tokio::time::sleep(Duration::from_millis(9)).await;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use crate::{repo::messages::CheckCIStatus, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
let next_commit = given::a_named_commit("next");
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
when::commit_status(
|
||||
&mut forge,
|
||||
next_commit.clone(),
|
||||
git::forge::commit::Status::Pass,
|
||||
);
|
||||
forge
|
||||
.expect_commit_status()
|
||||
.with(mockall::predicate::eq(next_commit.clone()))
|
||||
.return_once(|_| Ok(git::forge::commit::Status::Pass));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
|
@ -20,18 +21,11 @@ async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
|||
repo_details,
|
||||
Box::new(forge),
|
||||
);
|
||||
addr.send(crate::repo::messages::CheckCIStatus::new(
|
||||
next_commit.clone(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, CheckCIStatus::new(next_commit.clone()))?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: ReceiveCIStatus")));
|
||||
})?;
|
||||
log.require_message_containing("send: ReceiveCIStatus")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use kxio::net::Net;
|
||||
|
||||
use crate::tell;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_clone() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -23,13 +27,19 @@ async fn should_clone() -> TestResult {
|
|||
let _ = cloned_ref.write().map(|mut l| l.push(()));
|
||||
Ok(Box::new(open_repository))
|
||||
});
|
||||
let net: Net = given::a_network().into();
|
||||
|
||||
//when
|
||||
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
||||
addr.send(CloneRepo::new()).await?;
|
||||
System::current().stop();
|
||||
|
||||
let (addr, _log) = when::start_actor(
|
||||
repository_factory,
|
||||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
//then
|
||||
tick(1).await;
|
||||
cloned
|
||||
.read()
|
||||
.map_err(|e| e.to_string())
|
||||
|
@ -38,7 +48,7 @@ async fn should_clone() -> TestResult {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn should_open() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -60,13 +70,20 @@ async fn should_open() -> TestResult {
|
|||
Ok(Box::new(open_repository))
|
||||
});
|
||||
fs.dir(&repo_details.gitdir).create()?;
|
||||
let net: Net = given::a_network().into();
|
||||
|
||||
//when
|
||||
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
||||
addr.send(CloneRepo::new()).await?;
|
||||
System::current().stop();
|
||||
let (addr, _log) = when::start_actor(
|
||||
repository_factory,
|
||||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
|
||||
//then
|
||||
tick(1).await;
|
||||
opened
|
||||
.read()
|
||||
.map_err(|e| e.to_string())
|
||||
|
@ -78,7 +95,7 @@ async fn should_open() -> TestResult {
|
|||
/// The server config can optionally include the names of the main, next and dev
|
||||
/// branches. When it doesn't we should load the `.git-next.yaml` from from the
|
||||
/// repo and get the branch names from there by sending a [LoadConfigFromRepo] message.
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -95,21 +112,28 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
|
|||
let mut repository_factory = MockRepositoryFactory::new();
|
||||
expect::open_repository(&mut repository_factory, open_repository);
|
||||
fs.dir(&repo_details.gitdir).create()?;
|
||||
let net: Net = given::a_network().into();
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
||||
addr.send(CloneRepo::new()).await?;
|
||||
System::current().stop();
|
||||
let (addr, log) = when::start_actor(
|
||||
repository_factory,
|
||||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: LoadConfigFromRepo")?;
|
||||
log.require_message_containing("send: LoadConfigFromRepo")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The server config can optionally include the names of the main, next and dev
|
||||
/// branches. When it does we should register the webhook by sending [RegisterWebhook] message.
|
||||
#[actix::test]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -124,71 +148,23 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
|
|||
let mut repository_factory = MockRepositoryFactory::new();
|
||||
expect::open_repository(&mut repository_factory, open_repository);
|
||||
fs.dir(&repo_details.gitdir).create()?;
|
||||
let net: Net = given::a_network().into();
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
||||
addr.send(CloneRepo::new()).await?;
|
||||
System::current().stop();
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: RegisterWebhook")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
given::has_remote_defaults(
|
||||
&mut open_repository,
|
||||
HashMap::from([
|
||||
(Direction::Push, None),
|
||||
(Direction::Fetch, repo_details.remote_url()),
|
||||
]),
|
||||
let (addr, log) = when::start_actor(
|
||||
repository_factory,
|
||||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net,
|
||||
);
|
||||
|
||||
let mut repository_factory = MockRepositoryFactory::new();
|
||||
expect::open_repository(&mut repository_factory, open_repository);
|
||||
fs.dir(&repo_details.gitdir).create()?;
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
||||
addr.send(CloneRepo::new()).await?;
|
||||
System::current().stop();
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("open failed")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
|
||||
let mut repository_factory = MockRepositoryFactory::new();
|
||||
expect::open_repository(&mut repository_factory, open_repository);
|
||||
fs.dir(&repo_details.gitdir).create()?;
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
||||
addr.send(CloneRepo::new()).await?;
|
||||
System::current().stop();
|
||||
|
||||
//then
|
||||
log.require_message_containing("open failed")?;
|
||||
tick(1).await;
|
||||
debug!(?log, "");
|
||||
log.require_message_containing("send: RegisterWebhook")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,37 +1,33 @@
|
|||
use crate::{repo::messages::LoadConfigFromRepo, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
let mut load_config_open_repo = MockOpenRepositoryLike::new();
|
||||
// let mut load_config_open_repo = MockOpenRepositoryLike::new();
|
||||
let branches = given::repo_branches();
|
||||
let remote_branches = vec![branches.main(), branches.next(), branches.dev()];
|
||||
load_config_open_repo
|
||||
.expect_read_file()
|
||||
.return_once(move |_, _| {
|
||||
Ok(format!(
|
||||
r#"
|
||||
open_repository.expect_read_file().return_once(move |_, _| {
|
||||
Ok(format!(
|
||||
r#"
|
||||
[branches]
|
||||
main = "{}"
|
||||
next = "{}"
|
||||
dev = "{}"
|
||||
"#,
|
||||
branches.main(),
|
||||
branches.next(),
|
||||
branches.dev()
|
||||
))
|
||||
});
|
||||
|
||||
load_config_open_repo
|
||||
.expect_remote_branches()
|
||||
.return_once(|| Ok(remote_branches));
|
||||
branches.main(),
|
||||
branches.next(),
|
||||
branches.dev()
|
||||
))
|
||||
});
|
||||
|
||||
open_repository
|
||||
.expect_duplicate()
|
||||
.return_once(|| Box::new(load_config_open_repo));
|
||||
.expect_remote_branches()
|
||||
.return_once(|| Ok(remote_branches));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
|
@ -39,44 +35,36 @@ async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::LoadConfigFromRepo::new())
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, LoadConfigFromRepo::new())?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.require_message_containing("send: ReceiveRepoConfig")?;
|
||||
log.no_message_contains("send: NotifyUsers")?;
|
||||
log.require_message_containing("send: ReceiveRepoConfig")
|
||||
.await?;
|
||||
log.no_message_contains("send: NotifyUsers").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_read_file_err_should_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
let mut load_config_open_repo = MockOpenRepositoryLike::new();
|
||||
load_config_open_repo
|
||||
open_repository
|
||||
.expect_read_file()
|
||||
.return_once(move |_, _| Err(git::file::Error::FileNotFound));
|
||||
|
||||
open_repository
|
||||
.expect_duplicate()
|
||||
.return_once(|| Box::new(load_config_open_repo));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
Box::new(open_repository),
|
||||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::LoadConfigFromRepo::new())
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, LoadConfigFromRepo::new())?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.require_message_containing("send: NotifyUser")?;
|
||||
log.no_message_contains("send: ReceiveRepoConfig")?;
|
||||
log.require_message_containing("send: NotifyUser").await?;
|
||||
log.no_message_contains("send: ReceiveRepoConfig").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
//
|
||||
use crate::{repo::messages::ReceiveRepoConfig, tell};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn should_store_repo_config_in_actor() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -15,16 +17,15 @@ async fn should_store_repo_config_in_actor() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ReceiveRepoConfig::new(
|
||||
new_repo_config.clone(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
|
||||
let reo_actor_view = addr.send(ExamineActor).await?;
|
||||
let reo_actor_view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(
|
||||
reo_actor_view.repo_details.repo_config,
|
||||
Some(new_repo_config)
|
||||
|
@ -32,7 +33,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_register_webhook() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -46,14 +47,11 @@ async fn should_register_webhook() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ReceiveRepoConfig::new(
|
||||
new_repo_config.clone(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.require_message_containing("send: RegisterWebhook")?;
|
||||
log.require_message_containing("send: RegisterWebhook")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use git::forge::commit::Status;
|
||||
|
||||
use crate::{repo::messages::ReceiveCIStatus, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_pass_should_advance_main_to_next() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -16,24 +20,19 @@ async fn when_pass_should_advance_main_to_next() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
||||
next_commit.clone(),
|
||||
git::forge::commit::Status::Pass,
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
addr,
|
||||
ReceiveCIStatus::new((next_commit.clone(), Status::Pass))
|
||||
)?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
let expected = format!("send: AdvanceMain({next_commit:?})");
|
||||
tracing::debug!(%expected,"");
|
||||
assert!(l.iter().any(|message| message.contains(&expected)));
|
||||
})?;
|
||||
log.require_message_containing(format!("send: AdvanceMain({next_commit:?})"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_pending_should_recheck_ci_status() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -46,47 +45,43 @@ async fn when_pending_should_recheck_ci_status() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
||||
next_commit.clone(),
|
||||
git::forge::commit::Status::Pending,
|
||||
)))
|
||||
.await?;
|
||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
addr,
|
||||
ReceiveCIStatus::new((next_commit.clone(), Status::Pending))
|
||||
)?;
|
||||
tokio::time::sleep(Duration::from_millis(9)).await;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_fail_should_recheck_after_delay() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
let next_commit = given::a_named_commit("next");
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
let (repo_actor_ref, log) = when::start_actor_with_open_repository(
|
||||
Box::new(open_repository),
|
||||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
||||
next_commit.clone(),
|
||||
git::forge::commit::Status::Fail,
|
||||
)))
|
||||
.await?;
|
||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
||||
System::current().stop();
|
||||
|
||||
//when
|
||||
tell!(
|
||||
repo_actor_ref,
|
||||
ReceiveCIStatus::new((next_commit.clone(), Status::Fail))
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await; // otherwise this test can be flakey
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_fail_should_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -99,14 +94,12 @@ async fn when_fail_should_notify_user() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
||||
next_commit.clone(),
|
||||
git::forge::commit::Status::Fail,
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
addr,
|
||||
ReceiveCIStatus::new((next_commit.clone(), Status::Fail))
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: NotifyUser")?;
|
||||
log.require_message_containing("send: NotifyUser").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,71 +1,57 @@
|
|||
use crate::{repo::messages::RegisterWebhook, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
||||
let registered_webhook = given::a_registered_webhook();
|
||||
let mut my_forge = git::MockForgeLike::new();
|
||||
my_forge
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge
|
||||
.expect_register_webhook()
|
||||
.return_once(move |_| Ok(registered_webhook));
|
||||
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge.expect_duplicate().return_once(|| Box::new(my_forge));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
Box::new(open_repository),
|
||||
repo_details,
|
||||
Box::new(forge),
|
||||
);
|
||||
addr.send(crate::repo::messages::RegisterWebhook::new())
|
||||
.await?;
|
||||
System::current().stop();
|
||||
|
||||
tell!(addr, RegisterWebhook::new())?;
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: WebhookRegistered")));
|
||||
})?;
|
||||
log.require_message_containing("send: WebhookRegistered")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_registered_error_should_send_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
||||
let mut my_forge = git::MockForgeLike::new();
|
||||
my_forge.expect_register_webhook().return_once(move |_| {
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge.expect_register_webhook().return_once(move |_| {
|
||||
Err(git::forge::webhook::Error::FailedToRegister(
|
||||
"foo".to_string(),
|
||||
))
|
||||
});
|
||||
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge.expect_duplicate().return_once(|| Box::new(my_forge));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
Box::new(open_repository),
|
||||
repo_details,
|
||||
Box::new(forge),
|
||||
);
|
||||
addr.send(crate::repo::messages::RegisterWebhook::new())
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, crate::repo::messages::RegisterWebhook::new())?;
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read()
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|l| assert!(l.iter().any(|message| message.contains("send: NotifyUser"))))?;
|
||||
log.require_message_containing("send: NotifyUser").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
use crate::repo::messages::{AdvanceNext, AdvanceNextPayload};
|
||||
|
||||
//
|
||||
use kxio::net::Net;
|
||||
|
||||
use crate::{
|
||||
repo::messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
||||
tell,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main(
|
||||
) -> TestResult {
|
||||
//given
|
||||
|
@ -41,18 +46,15 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?;
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev(
|
||||
) -> TestResult {
|
||||
//given
|
||||
|
@ -92,11 +94,10 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
addr,
|
||||
crate::repo::messages::ValidateRepo::new(MessageToken::default())
|
||||
)?;
|
||||
|
||||
//then
|
||||
let expected = AdvanceNext::new(AdvanceNextPayload {
|
||||
|
@ -104,11 +105,12 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
|
|||
main: main_commit,
|
||||
dev_commit_history: dev_branch_log,
|
||||
});
|
||||
log.require_message_containing(format!("send: {expected:?}",))?;
|
||||
log.require_message_containing(format!("send: {expected:?}",))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -145,18 +147,15 @@ async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?;
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -193,18 +192,15 @@ async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?;
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -240,18 +236,15 @@ async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))?;
|
||||
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
|
||||
// Do nothing, when the situation changes we will hear about it via a webhook
|
||||
//given
|
||||
|
@ -288,18 +281,14 @@ async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send:")?;
|
||||
log.no_message_contains("send:").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -337,11 +326,7 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
let expected = AdvanceNext::new(AdvanceNextPayload {
|
||||
|
@ -349,11 +334,12 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
|||
main: main_commit,
|
||||
dev_commit_history: dev_branch_log,
|
||||
});
|
||||
log.require_message_containing(format!("send: {expected:?}"))?;
|
||||
log.require_message_containing(format!("send: {expected:?}"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -391,96 +377,87 @@ async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: NotifyUser")?;
|
||||
log.require_message_containing("send: NotifyUser").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_accept_message_with_current_token() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
||||
//when
|
||||
let net: Net = given::a_network().into();
|
||||
let server_actor_ref = given::a_server_actor(fs.as_real(), net.clone());
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
git::repository::factory::mock(),
|
||||
given::a_forge(),
|
||||
given::a_network().into(),
|
||||
server_actor_ref,
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
||||
let addr = actor.start();
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
|
||||
2_u32,
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
let addr = kameo::spawn(actor);
|
||||
//
|
||||
//when
|
||||
tell!(addr, ValidateRepo::new(MessageToken::new(2_u32)))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("accepted token: 2")?;
|
||||
log.require_message_containing("accepted token: 2").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_accept_message_with_new_token() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
let net: Net = given::a_network().into();
|
||||
|
||||
//when
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
git::repository::factory::mock(),
|
||||
given::a_forge(),
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
||||
let addr = actor.start();
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
|
||||
3_u32,
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
let addr = kameo::spawn(actor);
|
||||
tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("accepted token: 3")?;
|
||||
log.require_message_containing("accepted token: 3").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_reject_message_with_expired_token() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
let net: Net = given::a_network().into();
|
||||
|
||||
//when
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
git::repository::factory::mock(),
|
||||
given::a_forge(),
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_message_token(MessageToken::new(4_u32));
|
||||
let addr = actor.start();
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
|
||||
3_u32,
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
let addr = kameo::spawn(actor);
|
||||
tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("accepted token")?;
|
||||
log.no_message_contains("accepted token").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
// NOTE: failed then passed on retry: count = 6
|
||||
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
||||
//given
|
||||
|
@ -497,20 +474,16 @@ async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
|
||||
|
||||
//then
|
||||
log.require_message_containing("accepted token: 0")?;
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("accepted token: 0").await?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -546,15 +519,11 @@ async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
||||
MessageToken::default(),
|
||||
))
|
||||
.await?;
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
System::current().stop();
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
|
||||
//then
|
||||
log.require_message_containing("accepted token")?;
|
||||
log.require_message_containing("send: NotifyUser")?;
|
||||
log.require_message_containing("accepted token").await?;
|
||||
log.require_message_containing("send: NotifyUser").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use kxio::net::Net;
|
||||
|
||||
use crate::{repo::messages::WebhookNotification, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -12,30 +16,30 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
|||
let body = Body::new(String::new());
|
||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||
let repository_factory = MockRepositoryFactory::new();
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
given::a_forge(),
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(None);
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing("server has no auth token")?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("server has no auth token")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_no_repo_config_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -46,30 +50,30 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
|
|||
let body = Body::new(String::new());
|
||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||
let repository_factory = MockRepositoryFactory::new();
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
given::a_forge(),
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing("server has no repo config")?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("server has no repo config")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -84,30 +88,30 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
|||
forge
|
||||
.expect_is_message_authorised()
|
||||
.return_once(|_, _| false); // is not valid
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing("message authorisation is invalid")?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("message authorisation is invalid")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -126,30 +130,30 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
|||
forge
|
||||
.expect_parse_webhook_body()
|
||||
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing("forge sent ignorable message")?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("forge sent ignorable message")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -168,30 +172,30 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
|||
forge
|
||||
.expect_parse_webhook_body()
|
||||
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing("message parse error - not a push")?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("message parse error - not a push")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -214,32 +218,31 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_main_commit(commit);
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing("unknown branch")?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("unknown branch").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -263,32 +266,32 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_main_commit(commit);
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing(format!("not a new commit on {main}"))?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing(format!("not a new commit on {main}"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -301,9 +304,9 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
|||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||
let repository_factory = MockRepositoryFactory::new();
|
||||
let commit = given::a_commit();
|
||||
let next = repo_config.branches().next();
|
||||
let next_branch = repo_config.branches().next();
|
||||
let push = given::a_push()
|
||||
.with_branch(next.clone())
|
||||
.with_branch(next_branch.clone())
|
||||
.with_sha(commit.sha().to_string())
|
||||
.with_message(commit.message().to_string());
|
||||
let mut forge = given::a_forge();
|
||||
|
@ -312,32 +315,32 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_next_commit(commit);
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing(format!("not a new commit on {next}"))?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing(format!("not a new commit on {next_branch}"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -361,32 +364,32 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_dev_commit(commit);
|
||||
|
||||
//when
|
||||
actor
|
||||
.start()
|
||||
.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
kameo::spawn(actor),
|
||||
WebhookNotification::new(forge_notification)
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.no_message_contains("send")?;
|
||||
log.require_message_containing(format!("not a new commit on {dev}"))?;
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing(format!("not a new commit on {dev}"))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -409,32 +412,33 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_main_commit(given::a_commit());
|
||||
|
||||
//when
|
||||
let addr = actor.start();
|
||||
addr.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
let addr = kameo::spawn(actor);
|
||||
tell!(addr, WebhookNotification::new(forge_notification))?;
|
||||
|
||||
//then
|
||||
let view = addr.send(ExamineActor).await?;
|
||||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.last_main_commit, Some(push_commit));
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -457,32 +461,33 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_next_commit(given::a_commit());
|
||||
|
||||
//when
|
||||
let addr = actor.start();
|
||||
addr.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
let addr = kameo::spawn(actor);
|
||||
tell!(addr, WebhookNotification::new(forge_notification))?;
|
||||
|
||||
//then
|
||||
let view = addr.send(ExamineActor).await?;
|
||||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.last_next_commit, Some(push_commit));
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -505,27 +510,28 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
|
|||
.return_once(|_, _| true); // is valid
|
||||
forge.expect_should_ignore_message().returning(|_| false);
|
||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||
let net: Net = given::a_network().into();
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
.with_last_dev_commit(given::a_commit());
|
||||
|
||||
//when
|
||||
let addr = actor.start();
|
||||
addr.send(crate::repo::messages::WebhookNotification::new(
|
||||
forge_notification,
|
||||
))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
let addr = kameo::spawn(actor);
|
||||
tell!(addr, WebhookNotification::new(forge_notification))?;
|
||||
|
||||
//then
|
||||
let view = addr.send(ExamineActor).await?;
|
||||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.last_dev_commit, Some(push_commit));
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::{repo::messages::WebhookRegistered, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn should_store_webhook_details() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -15,21 +17,23 @@ async fn should_store_webhook_details() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::WebhookRegistered::new((
|
||||
webhook_id.clone(),
|
||||
webhook_auth.clone(),
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
addr,
|
||||
WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone()))
|
||||
)?;
|
||||
|
||||
//then
|
||||
let view = addr.send(ExamineActor).await?;
|
||||
// let view = addr.ask(ExamineActor).await.expect("examine actor");
|
||||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.webhook_id, Some(webhook_id));
|
||||
assert_eq!(view.webhook_auth, Some(webhook_auth));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn should_send_validate_repo_message() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -43,14 +47,12 @@ async fn should_send_validate_repo_message() -> TestResult {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
);
|
||||
addr.send(crate::repo::messages::WebhookRegistered::new((
|
||||
webhook_id.clone(),
|
||||
webhook_auth.clone(),
|
||||
)))
|
||||
.await?;
|
||||
System::current().stop();
|
||||
tell!(
|
||||
addr,
|
||||
WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone()))
|
||||
)?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use super::*;
|
|||
use crate::git::file;
|
||||
use crate::repo::load;
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_file_not_found_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -13,7 +13,7 @@ async fn when_file_not_found_should_error() -> TestResult {
|
|||
.expect_read_file()
|
||||
.returning(|_, _| Err(file::Error::FileNotFound));
|
||||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(
|
||||
|
@ -22,7 +22,7 @@ async fn when_file_not_found_should_error() -> TestResult {
|
|||
));
|
||||
Ok(())
|
||||
}
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_file_format_invalid_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -32,14 +32,14 @@ async fn when_file_format_invalid_should_error() -> TestResult {
|
|||
.expect_read_file()
|
||||
.return_once(move |_, _| Ok(contents));
|
||||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::Toml(_)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_main_branch_is_missing_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -64,14 +64,14 @@ async fn when_main_branch_is_missing_should_error() -> TestResult {
|
|||
.expect_remote_branches()
|
||||
.return_once(move || Ok(branches));
|
||||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_next_branch_is_missing_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -96,14 +96,14 @@ async fn when_next_branch_is_missing_should_error() -> TestResult {
|
|||
.expect_remote_branches()
|
||||
.return_once(move || Ok(branches));
|
||||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_dev_branch_is_missing_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -128,14 +128,14 @@ async fn when_dev_branch_is_missing_should_error() -> TestResult {
|
|||
.expect_remote_branches()
|
||||
.return_once(move || Ok(branches));
|
||||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn when_valid_file_should_return_repo_config() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -161,7 +161,7 @@ async fn when_valid_file_should_return_repo_config() -> TestResult {
|
|||
.expect_remote_branches()
|
||||
.return_once(move || Ok(branches));
|
||||
//when
|
||||
let_assert!(Ok(result) = load::config_from_repository(repo_details, &open_repository).await);
|
||||
let_assert!(Ok(result) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
debug!("Got: {result:?}");
|
||||
assert_eq!(result, repo_config);
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use crate::{
|
||||
git,
|
||||
repo::{
|
||||
|
@ -12,7 +10,6 @@ use crate::{
|
|||
use git_next_core::{
|
||||
git::{
|
||||
commit::Sha,
|
||||
forge::commit::Status,
|
||||
repository::{
|
||||
factory::{mock, MockRepositoryFactory, RepositoryFactory},
|
||||
open::{MockOpenRepositoryLike, OpenRepositoryLike},
|
||||
|
@ -28,12 +25,17 @@ use git_next_core::{
|
|||
};
|
||||
|
||||
use assert2::let_assert;
|
||||
use kameo::{
|
||||
message::{Context, Message},
|
||||
Reply,
|
||||
};
|
||||
use mockall::predicate::eq;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
@ -45,33 +47,43 @@ mod handlers;
|
|||
mod load;
|
||||
mod when;
|
||||
|
||||
pub async fn tick(millis: u64) {
|
||||
tokio::time::sleep(Duration::from_millis(millis)).await;
|
||||
}
|
||||
|
||||
impl ActorLog {
|
||||
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult {
|
||||
if self.find_in_messages(needle.as_ref())? {
|
||||
pub async fn no_message_contains(
|
||||
&self,
|
||||
needle: impl AsRef<str> + Send + std::fmt::Display,
|
||||
) -> TestResult {
|
||||
if self.find_in_messages(needle.as_ref()).await? {
|
||||
error!(?self, "");
|
||||
panic!("found unexpected message: {needle}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn require_message_containing(
|
||||
pub async fn require_message_containing(
|
||||
&self,
|
||||
needle: impl AsRef<str> + std::fmt::Display,
|
||||
needle: impl AsRef<str> + Send + std::fmt::Display,
|
||||
) -> TestResult {
|
||||
if !self.find_in_messages(needle.as_ref())? {
|
||||
if !self.find_in_messages(needle.as_ref()).await? {
|
||||
error!(?self, "");
|
||||
panic!("expected message not found: {needle}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_in_messages(
|
||||
async fn find_in_messages(
|
||||
&self,
|
||||
needle: impl AsRef<str>,
|
||||
needle: impl AsRef<str> + Send,
|
||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
// Very short sleep to allow tests to get a chance to tick
|
||||
// This should be enough for most tests.
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let found = self
|
||||
.read()
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.iter()
|
||||
.any(|message| message.contains(needle.as_ref()));
|
||||
Ok(found)
|
||||
|
@ -79,15 +91,19 @@ impl ActorLog {
|
|||
}
|
||||
|
||||
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
|
||||
impl Handler<ExamineActor> for RepoActor {
|
||||
type Result = RepoActorView;
|
||||
impl Message<ExamineActor> for RepoActor {
|
||||
type Reply = RepoActorView;
|
||||
|
||||
fn handle(&mut self, _msg: ExamineActor, _ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: ExamineActor,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let repo_actor: &Self = self;
|
||||
Self::Result::from(repo_actor)
|
||||
Self::Reply::from(repo_actor)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, MessageResponse)]
|
||||
#[derive(Debug, Reply)]
|
||||
pub struct RepoActorView {
|
||||
pub repo_details: RepoDetails,
|
||||
pub webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
//
|
||||
use kameo::actor::ActorRef;
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
|
||||
use crate::server::ServerActor;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn start_actor(
|
||||
repository_factory: MockRepositoryFactory,
|
||||
repo_details: RepoDetails,
|
||||
forge: Box<dyn ForgeLike>,
|
||||
) -> (actix::Addr<RepoActor>, ActorLog) {
|
||||
fs: FileSystem,
|
||||
net: Net,
|
||||
) -> (ActorRef<RepoActor>, ActorLog) {
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_network().into(),
|
||||
given::a_server_actor(fs, net.clone()),
|
||||
net,
|
||||
);
|
||||
(actor.start(), log)
|
||||
(kameo::spawn(actor), log)
|
||||
}
|
||||
|
||||
pub fn start_actor_with_open_repository(
|
||||
open_repository: Box<dyn OpenRepositoryLike>,
|
||||
repo_details: RepoDetails,
|
||||
forge: Box<dyn ForgeLike>,
|
||||
) -> (actix::Addr<RepoActor>, ActorLog) {
|
||||
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into());
|
||||
) -> (ActorRef<RepoActor>, ActorLog) {
|
||||
let fs = given::a_filesystem();
|
||||
let net: Net = given::a_network().into();
|
||||
let server_actor_ref: ActorRef<ServerActor> = given::a_server_actor(fs.as_real(), net.clone());
|
||||
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, server_actor_ref, net);
|
||||
let actor = actor.with_open_repository(Some(open_repository));
|
||||
(actor.start(), log)
|
||||
}
|
||||
|
||||
pub fn commit_status(forge: &mut MockForgeLike, commit: Commit, status: Status) {
|
||||
let mut commit_status_forge = MockForgeLike::new();
|
||||
commit_status_forge
|
||||
.expect_commit_status()
|
||||
.with(mockall::predicate::eq(commit))
|
||||
.return_once(|_| Ok(status));
|
||||
forge
|
||||
.expect_duplicate()
|
||||
.return_once(move || Box::new(commit_status_forge));
|
||||
(kameo::spawn(actor), log)
|
||||
}
|
||||
|
|
221
crates/cli/src/root.rs
Normal file
221
crates/cli/src/root.rs
Normal file
|
@ -0,0 +1,221 @@
|
|||
//
|
||||
|
||||
// Root actor for all other actors - supervises them all
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use derive_more::derive::Constructor;
|
||||
use git_next_core::{git::RepositoryFactory, s};
|
||||
use kameo::{
|
||||
actor::{pubsub::PubSub, ActorRef},
|
||||
mailbox::unbounded::UnboundedMailbox,
|
||||
message::{Context, Message},
|
||||
Actor,
|
||||
};
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
use crate::tui::Tui;
|
||||
use crate::{
|
||||
alerts::{AlertsActor, History},
|
||||
base_actor::BaseActor,
|
||||
default_on_actor_panic, default_on_actor_start,
|
||||
file_watcher::{FileUpdated, FileWatcherActor},
|
||||
on_actor_link_died, on_actor_stop, publish,
|
||||
server::{actor::messages::ServerUpdate, ServerActor},
|
||||
subscribe, MessageBus,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RootActor {
|
||||
ui: bool,
|
||||
fs: FileSystem,
|
||||
net: Net,
|
||||
sleep_duration: std::time::Duration,
|
||||
tx_shutdown: tokio::sync::mpsc::Sender<String>,
|
||||
|
||||
alerts_actor_ref: Option<ActorRef<AlertsActor>>,
|
||||
server_updates_bus: Option<MessageBus<ServerUpdate>>,
|
||||
server_actor_ref: Option<ActorRef<ServerActor>>,
|
||||
file_updates_bus: Option<MessageBus<FileUpdated>>,
|
||||
file_watcher_actor_ref: Option<ActorRef<FileWatcherActor>>,
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
tui_actor_ref: Option<ActorRef<Tui>>,
|
||||
}
|
||||
|
||||
impl RootActor {
|
||||
pub const fn new(
|
||||
ui: bool,
|
||||
fs: FileSystem,
|
||||
net: Net,
|
||||
sleep_duration: std::time::Duration,
|
||||
tx_shutdown: tokio::sync::mpsc::Sender<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ui,
|
||||
fs,
|
||||
net,
|
||||
sleep_duration,
|
||||
tx_shutdown,
|
||||
|
||||
alerts_actor_ref: None,
|
||||
server_updates_bus: None,
|
||||
server_actor_ref: None,
|
||||
file_updates_bus: None,
|
||||
file_watcher_actor_ref: None,
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
tui_actor_ref: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
type RootContext<'a> = Context<'a, RootActor, Result<()>>;
|
||||
|
||||
#[derive(Constructor, Debug)]
|
||||
pub struct Start {
|
||||
repo: Box<dyn RepositoryFactory>,
|
||||
}
|
||||
|
||||
impl Message<Start> for RootActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
async fn handle(&mut self, msg: Start, ctx: RootContext<'_>) -> Self::Reply {
|
||||
let alerts_actor_ref = self.start_alerts_actor(&ctx).await?;
|
||||
|
||||
let server_updates_bus = self.start_server_updates_bus(&ctx).await;
|
||||
let file_updates_bus = self.start_file_updates_bus(&ctx).await;
|
||||
|
||||
self.start_server_actor(
|
||||
&ctx,
|
||||
alerts_actor_ref,
|
||||
server_updates_bus.clone(),
|
||||
file_updates_bus.clone(),
|
||||
msg.repo,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.start_file_watcher_actor(&ctx, file_updates_bus.clone())
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
if self.ui {
|
||||
self.start_tui_actor(&ctx, server_updates_bus).await?;
|
||||
}
|
||||
|
||||
// trigger initial config file to load
|
||||
publish!(file_updates_bus, FileUpdated)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl RootActor {
|
||||
async fn start_alerts_actor(&mut self, ctx: &RootContext<'_>) -> Result<ActorRef<AlertsActor>> {
|
||||
let actor_ref = AlertsActor::new(None, History::new(A_DAY), self.net.clone())
|
||||
.spawn(ctx.actor_ref())
|
||||
.await;
|
||||
self.alerts_actor_ref.replace(actor_ref.clone());
|
||||
Ok(actor_ref)
|
||||
}
|
||||
|
||||
async fn start_file_watcher_actor(
|
||||
&mut self,
|
||||
ctx: &RootContext<'_>,
|
||||
file_updates_bus: MessageBus<FileUpdated>,
|
||||
) -> ActorRef<FileWatcherActor> {
|
||||
let actor_ref = FileWatcherActor::new(
|
||||
file_updates_bus,
|
||||
self.fs.base().join("git-next-server.toml"),
|
||||
)
|
||||
.spawn_in_thread(ctx.actor_ref())
|
||||
.await;
|
||||
self.file_watcher_actor_ref.replace(actor_ref.clone());
|
||||
actor_ref
|
||||
}
|
||||
|
||||
async fn start_server_updates_bus(
|
||||
&mut self,
|
||||
ctx: &RootContext<'_>,
|
||||
) -> MessageBus<ServerUpdate> {
|
||||
let actor_ref = PubSub::<ServerUpdate>::new().spawn(ctx.actor_ref()).await;
|
||||
self.server_updates_bus.replace(actor_ref.clone());
|
||||
actor_ref
|
||||
}
|
||||
|
||||
async fn start_file_updates_bus(&mut self, ctx: &RootContext<'_>) -> MessageBus<FileUpdated> {
|
||||
let actor_ref = PubSub::<FileUpdated>::new().spawn(ctx.actor_ref()).await;
|
||||
self.file_updates_bus.replace(actor_ref.clone());
|
||||
actor_ref
|
||||
}
|
||||
|
||||
async fn start_server_actor(
|
||||
&mut self,
|
||||
ctx: &RootContext<'_>,
|
||||
alerts_actor_ref: ActorRef<AlertsActor>,
|
||||
server_updates_bus: MessageBus<ServerUpdate>,
|
||||
file_updates_bus: MessageBus<FileUpdated>,
|
||||
repo: Box<dyn RepositoryFactory>,
|
||||
) -> Result<ActorRef<ServerActor>> {
|
||||
let actor_ref = ServerActor::new(
|
||||
self.ui,
|
||||
self.fs.clone(),
|
||||
self.net.clone(),
|
||||
alerts_actor_ref,
|
||||
server_updates_bus,
|
||||
repo,
|
||||
self.sleep_duration,
|
||||
)
|
||||
.spawn(ctx.actor_ref())
|
||||
.await;
|
||||
subscribe!(file_updates_bus, "server", actor_ref.clone())?;
|
||||
self.server_actor_ref.replace(actor_ref.clone());
|
||||
Ok(actor_ref)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
async fn start_tui_actor(
|
||||
&mut self,
|
||||
ctx: &RootContext<'_>,
|
||||
server_updates_bus: MessageBus<ServerUpdate>,
|
||||
) -> Result<ActorRef<Tui>> {
|
||||
let actor_ref = Tui::new().spawn_in_thread(ctx.actor_ref()).await;
|
||||
self.tui_actor_ref.replace(actor_ref.clone());
|
||||
subscribe!(server_updates_bus, actor_ref.clone())?;
|
||||
Ok(actor_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for RootActor {
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
|
||||
default_on_actor_start!(this, actor_ref);
|
||||
default_on_actor_panic!(this, actor_ref, err);
|
||||
|
||||
on_actor_link_died!(this, actor_ref, id, reason, {
|
||||
this.tx_shutdown.send(s!("link died")).await?;
|
||||
match &reason {
|
||||
kameo::error::ActorStopReason::Normal => Ok(None),
|
||||
kameo::error::ActorStopReason::Killed
|
||||
| kameo::error::ActorStopReason::Panicked(_)
|
||||
| kameo::error::ActorStopReason::LinkDied { .. } => {
|
||||
Ok(Some(kameo::error::ActorStopReason::LinkDied {
|
||||
id,
|
||||
reason: Box::new(reason),
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on_actor_stop!(this, actor_ref, reason, {
|
||||
#[allow(clippy::expect_used)]
|
||||
this.tx_shutdown
|
||||
.send(s!("stopping"))
|
||||
.await
|
||||
.expect("send shutdown");
|
||||
Ok(())
|
||||
});
|
||||
}
|
|
@ -1,20 +1,36 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
use git_next_core::server::AppConfig;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
file_watcher::FileUpdated,
|
||||
server::actor::{messages::ReceiveAppConfig, ServerActor},
|
||||
tell,
|
||||
};
|
||||
|
||||
impl Handler<FileUpdated> for ServerActor {
|
||||
type Result = ();
|
||||
impl Message<FileUpdated> for ServerActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: FileUpdated,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
debug!("FileUpdated");
|
||||
match AppConfig::load(&self.fs) {
|
||||
Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx),
|
||||
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")),
|
||||
};
|
||||
Ok(app_config) => Ok(tell!(
|
||||
"server",
|
||||
ctx.actor_ref(),
|
||||
ReceiveAppConfig::new(app_config)
|
||||
)?),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to load config file. Error: {err}");
|
||||
ctx.actor_ref().kill();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,3 @@ mod receive_valid_app_config;
|
|||
mod server_update;
|
||||
mod shutdown;
|
||||
mod shutdown_trigger;
|
||||
mod subscribe_updates;
|
||||
|
|
|
@ -1,35 +1,46 @@
|
|||
use actix::prelude::*;
|
||||
//
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
use crate::server::actor::{
|
||||
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
|
||||
ServerActor,
|
||||
use crate::{
|
||||
server::actor::{
|
||||
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
|
||||
ServerActor,
|
||||
},
|
||||
tell,
|
||||
};
|
||||
|
||||
impl Handler<ReceiveAppConfig> for ServerActor {
|
||||
type Result = ();
|
||||
impl Message<ReceiveAppConfig> for ServerActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||
tracing::info!("recieved server config");
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ReceiveAppConfig,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Ok(socket_addr) = msg.listen_socket_addr() else {
|
||||
return self.abort(ctx, "Unable to parse http.addr");
|
||||
self.abort(&ctx, "Unable to parse http.addr").await?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(server_storage) = self.server_storage(&msg) else {
|
||||
return self.abort(ctx, "Server storage not available");
|
||||
self.abort(&ctx, "Server storage not available").await?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if msg.listen().url().ends_with('/') {
|
||||
return self.abort(ctx, "webhook.url must not end with a '/'");
|
||||
self.abort(&ctx, "webhook.url must not end with a '/'")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.do_send(
|
||||
tell!(
|
||||
"server",
|
||||
ctx.actor_ref(),
|
||||
ReceiveValidAppConfig::new(ValidAppConfig::new(
|
||||
msg.peel(),
|
||||
socket_addr,
|
||||
server_storage,
|
||||
)),
|
||||
ctx,
|
||||
);
|
||||
))
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,97 +1,111 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use color_eyre::Result;
|
||||
use kameo::{
|
||||
actor::ActorRef,
|
||||
message::{Context, Message},
|
||||
};
|
||||
|
||||
use git_next_core::{ForgeAlias, RepoAlias};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
alerts::messages::UpdateShout,
|
||||
base_actor::BaseActor as _,
|
||||
repo::{messages::CloneRepo, RepoActor},
|
||||
server::actor::{
|
||||
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
|
||||
ServerActor,
|
||||
},
|
||||
webhook::{
|
||||
messages::ShutdownWebhook,
|
||||
router::{AddWebhookRecipient, WebhookRouterActor},
|
||||
WebhookActor,
|
||||
},
|
||||
spawn, tell,
|
||||
webhook::{self, router::AddWebhookRecipient, WebhookActor},
|
||||
};
|
||||
|
||||
impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||
type Result = ();
|
||||
impl Message<ReceiveValidAppConfig> for ServerActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ReceiveValidAppConfig,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let ValidAppConfig {
|
||||
app_config,
|
||||
socket_address,
|
||||
storage: server_storage,
|
||||
} = msg.peel();
|
||||
// shutdown any existing webhook actor
|
||||
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
|
||||
webhook_actor_addr.do_send(ShutdownWebhook);
|
||||
if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() {
|
||||
webhook_actor_ref.kill();
|
||||
}
|
||||
self.generation.inc();
|
||||
|
||||
// Webhook Server
|
||||
info!("Starting Webhook Server...");
|
||||
let webhook_router = WebhookRouterActor::default().start();
|
||||
let listen_url = app_config.listen().url();
|
||||
let notify_user_recipient = self.alerts.clone().recipient();
|
||||
let server_addr = Some(ctx.address());
|
||||
// Forge Actors
|
||||
for (forge_alias, forge_config) in app_config.forges() {
|
||||
let repo_actors = self
|
||||
.create_forge_repos(
|
||||
forge_config,
|
||||
forge_alias.clone(),
|
||||
&server_storage,
|
||||
listen_url,
|
||||
¬ify_user_recipient,
|
||||
server_addr.clone(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(start_repo_actor)
|
||||
.collect::<Vec<_>>();
|
||||
repo_actors
|
||||
.iter()
|
||||
.map(|(repo_alias, addr)| {
|
||||
AddWebhookRecipient::new(
|
||||
if let Some(webhook_router_actor_ref) = &self.webhook_router_actor_ref {
|
||||
let listen_url = app_config.listen().url();
|
||||
let server_actor_ref = ctx.actor_ref();
|
||||
// Forge Actors
|
||||
for (forge_alias, forge_config) in app_config.forges() {
|
||||
let repo_actors = self
|
||||
.create_forge_repos(
|
||||
forge_config,
|
||||
forge_alias.clone(),
|
||||
repo_alias.clone(),
|
||||
addr.clone().recipient(),
|
||||
&server_storage,
|
||||
listen_url,
|
||||
&self.alerts,
|
||||
server_actor_ref.clone(),
|
||||
)
|
||||
})
|
||||
.for_each(|msg| webhook_router.do_send(msg));
|
||||
for (repo_alias, addr) in repo_actors {
|
||||
self.repo_actors
|
||||
.insert((forge_alias.clone(), repo_alias), addr);
|
||||
.into_iter()
|
||||
.map(|repo_actor_tuple| {
|
||||
start_repo_actor(repo_actor_tuple, server_actor_ref.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for repo_actor in repo_actors {
|
||||
let (repo_alias, repo_actor_ref) = repo_actor.await?;
|
||||
tell!(
|
||||
webhook_router_actor_ref,
|
||||
AddWebhookRecipient::new(
|
||||
forge_alias.clone(),
|
||||
repo_alias.clone(),
|
||||
repo_actor_ref.clone(),
|
||||
)
|
||||
)?;
|
||||
|
||||
self.repo_actors
|
||||
.insert((forge_alias.clone(), repo_alias), repo_actor_ref);
|
||||
}
|
||||
}
|
||||
let webhook_actor_ref = spawn!(
|
||||
ctx.actor_ref(),
|
||||
WebhookActor::new(socket_address, webhook_router_actor_ref.clone())
|
||||
);
|
||||
tell!(webhook_actor_ref, webhook::Start)?;
|
||||
self.webhook_actor_ref.replace(webhook_actor_ref);
|
||||
}
|
||||
let webhook_actor_addr =
|
||||
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
||||
self.webhook_actor_addr.replace(webhook_actor_addr);
|
||||
let shout = app_config.shout().clone();
|
||||
self.app_config.replace(app_config.clone());
|
||||
self.do_send(
|
||||
tell!(
|
||||
"server",
|
||||
ctx.actor_ref(),
|
||||
ServerUpdate::AppConfigLoaded {
|
||||
app_config: ValidAppConfig {
|
||||
app_config,
|
||||
socket_address,
|
||||
storage: server_storage,
|
||||
},
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
self.alerts.do_send(UpdateShout::new(shout));
|
||||
}
|
||||
)?;
|
||||
tell!("alert", self.alerts, UpdateShout::new(shout))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn start_repo_actor(actor: (ForgeAlias, RepoAlias, RepoActor)) -> (RepoAlias, Addr<RepoActor>) {
|
||||
let (forge_name, repo_alias, actor) = actor;
|
||||
async fn start_repo_actor(
|
||||
actor: (ForgeAlias, RepoAlias, RepoActor),
|
||||
server_actor_ref: ActorRef<ServerActor>,
|
||||
) -> Result<(RepoAlias, ActorRef<RepoActor>)> {
|
||||
let (forge_name, repo_alias, repo_actor) = actor;
|
||||
let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias);
|
||||
let _guard = span.enter();
|
||||
let addr = actor.start();
|
||||
addr.do_send(CloneRepo);
|
||||
tracing::info!("Started");
|
||||
(repo_alias, addr)
|
||||
let repo_actor_ref = repo_actor.spawn(server_actor_ref).await;
|
||||
tell!(repo_actor_ref, CloneRepo)?;
|
||||
Ok((repo_alias, repo_actor_ref))
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
use actix::Handler;
|
||||
|
||||
//
|
||||
use crate::server::{actor::messages::ServerUpdate, ServerActor};
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
impl Handler<ServerUpdate> for ServerActor {
|
||||
type Result = ();
|
||||
use crate::{
|
||||
publish,
|
||||
server::{actor::messages::ServerUpdate, ServerActor},
|
||||
};
|
||||
|
||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.subscribers.iter().for_each(move |subscriber| {
|
||||
subscriber.do_send(msg.clone());
|
||||
});
|
||||
impl Message<ServerUpdate> for ServerActor {
|
||||
type Reply = color_eyre::Result<()>;
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ServerUpdate,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
publish!("server_updates_bus", self.server_updates_bus, msg)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
//-
|
||||
|
||||
use actix::prelude::*;
|
||||
use tracing::debug;
|
||||
//
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
repo::messages::UnRegisterWebhook,
|
||||
server::actor::{messages::Shutdown, ServerActor},
|
||||
webhook::messages::ShutdownWebhook,
|
||||
tell,
|
||||
};
|
||||
|
||||
impl Handler<Shutdown> for ServerActor {
|
||||
type Result = ();
|
||||
impl Message<Shutdown> for ServerActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
fn handle(&mut self, _msg: Shutdown, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.repo_actors
|
||||
.iter()
|
||||
.for_each(|((forge_alias, repo_alias), addr)| {
|
||||
debug!(%forge_alias, %repo_alias, "removing webhook");
|
||||
addr.do_send(UnRegisterWebhook::new());
|
||||
debug!(%forge_alias, %repo_alias, "removed webhook");
|
||||
});
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: Shutdown,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
for ((forge_alias, repo_alias), repo_actor_ref) in &self.repo_actors {
|
||||
debug!(%forge_alias, %repo_alias, "removing webhook");
|
||||
tell!(repo_actor_ref, UnRegisterWebhook::new())?;
|
||||
debug!(%forge_alias, %repo_alias, "removed webhook");
|
||||
repo_actor_ref.kill();
|
||||
info!(%forge_alias, %repo_alias, "killed repo actor");
|
||||
}
|
||||
debug!("server shutdown");
|
||||
if let Some(webhook) = self.webhook_actor_addr.take() {
|
||||
if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() {
|
||||
debug!("shutting down webhook");
|
||||
webhook.do_send(ShutdownWebhook);
|
||||
webhook_actor_ref.kill();
|
||||
debug!("webhook shutdown");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
//
|
||||
use actix::Handler;
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
|
||||
|
||||
impl Handler<ShutdownTrigger> for ServerActor {
|
||||
type Result = ();
|
||||
impl Message<ShutdownTrigger> for ServerActor {
|
||||
type Reply = ();
|
||||
|
||||
fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ShutdownTrigger,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
self.shutdown_trigger.replace(msg.peel());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
use crate::server::actor::{messages::SubscribeToUpdates, ServerActor};
|
||||
|
||||
//
|
||||
impl actix::Handler<SubscribeToUpdates> for ServerActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.subscribers.push(msg.peel());
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
//
|
||||
use actix::{Message, Recipient};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use derive_more::Constructor;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use git_next_core::{
|
||||
git::{self, forge::commit::Status, graph::Log, Commit},
|
||||
|
@ -10,8 +12,6 @@ use git_next_core::{
|
|||
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
||||
};
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
// receive server config
|
||||
message!(
|
||||
ReceiveAppConfig,
|
||||
|
@ -38,8 +38,7 @@ message!(
|
|||
|
||||
message!(Shutdown, "Notification to shutdown the server actor");
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Message)]
|
||||
#[rtype(result = "()")]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ServerUpdate {
|
||||
/// List of all configured forges and aliases
|
||||
AppConfigLoaded { app_config: ValidAppConfig },
|
||||
|
@ -96,18 +95,11 @@ pub enum RepoUpdate {
|
|||
MainUpdated,
|
||||
}
|
||||
|
||||
message!(
|
||||
SubscribeToUpdates,
|
||||
Recipient<ServerUpdate>,
|
||||
"Subscribe to receive updates from the server"
|
||||
);
|
||||
|
||||
/// Sends a channel to be used to shutdown the server
|
||||
#[derive(Message, Constructor)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
|
||||
#[derive(Constructor)]
|
||||
pub struct ShutdownTrigger(Sender<String>);
|
||||
impl ShutdownTrigger {
|
||||
pub fn peel(self) -> std::sync::mpsc::Sender<String> {
|
||||
pub fn peel(self) -> Sender<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,33 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Context, Actor};
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::{error, instrument, warn};
|
||||
|
||||
use git_next_core::{
|
||||
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
||||
s,
|
||||
server::{self, AppConfig, ListenUrl, Storage},
|
||||
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
alerts::AlertsActor,
|
||||
forge::Forge,
|
||||
on_actor_link_died, on_actor_panic, on_actor_start, on_actor_stop,
|
||||
repo::RepoActor,
|
||||
spawn, tell,
|
||||
webhook::{router::WebhookRouterActor, WebhookActor},
|
||||
MessageBus,
|
||||
};
|
||||
|
||||
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -9,25 +35,6 @@ mod tests;
|
|||
mod handlers;
|
||||
pub mod messages;
|
||||
|
||||
use crate::{
|
||||
alerts::messages::NotifyUser, alerts::AlertsActor, forge::Forge, repo::RepoActor,
|
||||
webhook::WebhookActor,
|
||||
};
|
||||
|
||||
use git_next_core::{
|
||||
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
||||
server::{self, AppConfig, ListenUrl, Storage},
|
||||
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
||||
};
|
||||
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
#[derive(Debug, derive_more::Display, derive_more::From)]
|
||||
pub enum Error {
|
||||
#[display("Failed to create data directories")]
|
||||
|
@ -41,65 +48,112 @@ pub enum Error {
|
|||
Config(server::Error),
|
||||
|
||||
Io(std::io::Error),
|
||||
|
||||
General(String),
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(derive_with::With)]
|
||||
#[with(message_log)]
|
||||
pub struct ServerActor {
|
||||
ui: bool,
|
||||
app_config: Option<AppConfig>,
|
||||
generation: Generation,
|
||||
webhook_actor_addr: Option<Addr<WebhookActor>>,
|
||||
webhook_actor_ref: Option<ActorRef<WebhookActor>>,
|
||||
fs: FileSystem,
|
||||
net: Net,
|
||||
alerts: Addr<AlertsActor>,
|
||||
alerts: ActorRef<AlertsActor>,
|
||||
repository_factory: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
||||
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), ActorRef<RepoActor>>,
|
||||
|
||||
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>,
|
||||
subscribers: Vec<Recipient<ServerUpdate>>,
|
||||
shutdown_trigger: Option<Sender<String>>,
|
||||
server_updates_bus: MessageBus<ServerUpdate>,
|
||||
webhook_router_actor_ref: Option<ActorRef<WebhookRouterActor>>,
|
||||
|
||||
// testing
|
||||
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
||||
}
|
||||
|
||||
impl Actor for ServerActor {
|
||||
type Context = Context<Self>;
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
|
||||
on_actor_start!(this, actor_ref, {
|
||||
let webhook_router = spawn!(actor_ref, WebhookRouterActor::default());
|
||||
this.webhook_router_actor_ref.replace(webhook_router);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
on_actor_panic!(this, actor_ref, err, {
|
||||
warn!(%err, "paniced: {}", <Self as Actor>::name());
|
||||
Ok(Some(kameo::error::ActorStopReason::Panicked(err)))
|
||||
});
|
||||
|
||||
on_actor_link_died!(this, actor_ref, id, reason, {
|
||||
match &reason {
|
||||
kameo::error::ActorStopReason::Killed | kameo::error::ActorStopReason::Normal => {
|
||||
Ok(None)
|
||||
}
|
||||
kameo::error::ActorStopReason::Panicked(_)
|
||||
| kameo::error::ActorStopReason::LinkDied { .. } => {
|
||||
if let Some(actor_ref) = actor_ref.upgrade() {
|
||||
tell!(actor_ref, Shutdown)?;
|
||||
}
|
||||
if let Some(trigger) = this.shutdown_trigger.take() {
|
||||
trigger
|
||||
.send(s!("link died"))
|
||||
.await
|
||||
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
|
||||
}
|
||||
Ok(Some(kameo::error::ActorStopReason::LinkDied {
|
||||
id,
|
||||
reason: Box::new(reason),
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on_actor_stop!(this, actor_ref, reason, Ok(()));
|
||||
}
|
||||
|
||||
impl ServerActor {
|
||||
pub fn new(
|
||||
ui: bool,
|
||||
fs: FileSystem,
|
||||
net: Net,
|
||||
alerts: Addr<AlertsActor>,
|
||||
alerts: ActorRef<AlertsActor>,
|
||||
server_updates_bus: MessageBus<ServerUpdate>,
|
||||
repo: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
) -> Self {
|
||||
let generation = Generation::default();
|
||||
Self {
|
||||
ui,
|
||||
app_config: None,
|
||||
generation,
|
||||
webhook_actor_addr: None,
|
||||
generation: Generation::default(),
|
||||
webhook_actor_ref: None,
|
||||
fs,
|
||||
net,
|
||||
alerts,
|
||||
repository_factory: repo,
|
||||
shutdown_trigger: None,
|
||||
subscribers: Vec::default(),
|
||||
server_updates_bus,
|
||||
webhook_router_actor_ref: None,
|
||||
sleep_duration,
|
||||
repo_actors: BTreeMap::new(),
|
||||
message_log: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_forge_data_directories(
|
||||
&self,
|
||||
app_config: &AppConfig,
|
||||
server_dir: &std::path::Path,
|
||||
) -> Result<()> {
|
||||
for (forge_name, _forge_config) in app_config.forges() {
|
||||
let forge_dir: PathBuf = (&forge_name).into();
|
||||
let path = server_dir.join(&forge_dir);
|
||||
let path = server_dir.join(PathBuf::from(&forge_name));
|
||||
let path_handle = self.fs.path(&path);
|
||||
if path_handle.exists()? {
|
||||
if !path_handle.is_dir()? {
|
||||
|
@ -110,45 +164,37 @@ impl ServerActor {
|
|||
self.fs.dir(&path).create_all()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn create_forge_repos(
|
||||
&self,
|
||||
forge_config: &ForgeConfig,
|
||||
forge_name: ForgeAlias,
|
||||
server_storage: &Storage,
|
||||
listen_url: &ListenUrl,
|
||||
notify_user_recipient: &Recipient<NotifyUser>,
|
||||
server_addr: Option<Addr<Self>>,
|
||||
notify_user_recipient: &ActorRef<AlertsActor>,
|
||||
server_actor_ref: ActorRef<Self>,
|
||||
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
||||
let span =
|
||||
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
||||
|
||||
let _guard = span.enter();
|
||||
tracing::info!("Creating Forge");
|
||||
let mut repos = vec![];
|
||||
tracing::info!(%forge_name, %forge_config, "");
|
||||
let creator = self.create_actor(
|
||||
forge_name,
|
||||
forge_config.clone(),
|
||||
server_storage,
|
||||
listen_url,
|
||||
server_addr,
|
||||
server_actor_ref,
|
||||
);
|
||||
for (repo_alias, server_repo_config) in forge_config.repos() {
|
||||
let forge_repo = creator((
|
||||
repo_alias,
|
||||
server_repo_config,
|
||||
notify_user_recipient.clone(),
|
||||
));
|
||||
tracing::info!(
|
||||
alias = %forge_repo.1,
|
||||
"Created Repo"
|
||||
);
|
||||
repos.push(forge_repo);
|
||||
}
|
||||
repos
|
||||
forge_config
|
||||
.repos()
|
||||
.map(|(repo_alias, server_repo_config)| {
|
||||
creator((
|
||||
repo_alias,
|
||||
server_repo_config,
|
||||
notify_user_recipient.clone(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn create_actor(
|
||||
|
@ -157,9 +203,9 @@ impl ServerActor {
|
|||
forge_config: ForgeConfig,
|
||||
server_storage: &Storage,
|
||||
listen_url: &ListenUrl,
|
||||
server_addr: Option<Addr<Self>>,
|
||||
server_actor_ref: ActorRef<Self>,
|
||||
) -> impl Fn(
|
||||
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
||||
(RepoAlias, &ServerRepoConfig, ActorRef<AlertsActor>),
|
||||
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
||||
let server_storage = server_storage.clone();
|
||||
let listen_url = listen_url.clone();
|
||||
|
@ -167,22 +213,19 @@ impl ServerActor {
|
|||
let repository_factory = self.repository_factory.duplicate();
|
||||
let generation = self.generation;
|
||||
let sleep_duration = self.sleep_duration;
|
||||
let ui = self.ui;
|
||||
move |(repo_alias, server_repo_config, notify_user_recipient)| {
|
||||
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
|
||||
let _guard = span.enter();
|
||||
tracing::info!("Creating Repo");
|
||||
let gitdir = server_repo_config.gitdir().map_or_else(
|
||||
|| {
|
||||
GitDir::new(
|
||||
server_storage
|
||||
.path()
|
||||
.join(forge_name.to_string())
|
||||
.join(repo_alias.to_string()),
|
||||
StoragePathType::Internal,
|
||||
)
|
||||
},
|
||||
|gitdir| gitdir,
|
||||
);
|
||||
let gitdir = server_repo_config.gitdir().unwrap_or_else(|| {
|
||||
GitDir::new(
|
||||
server_storage
|
||||
.path()
|
||||
.join(forge_name.to_string())
|
||||
.join(repo_alias.to_string()),
|
||||
StoragePathType::Internal,
|
||||
)
|
||||
});
|
||||
// INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not
|
||||
// have cloned the repo yet
|
||||
let repo_details = RepoDetails::new(
|
||||
|
@ -194,8 +237,8 @@ impl ServerActor {
|
|||
gitdir,
|
||||
);
|
||||
let forge = Forge::create(repo_details.clone(), net.clone());
|
||||
tracing::info!("Starting Repo Actor");
|
||||
let actor = RepoActor::new(
|
||||
ui,
|
||||
repo_details,
|
||||
forge,
|
||||
listen_url.clone(),
|
||||
|
@ -204,7 +247,7 @@ impl ServerActor {
|
|||
repository_factory.duplicate(),
|
||||
sleep_duration,
|
||||
Some(notify_user_recipient),
|
||||
server_addr.clone(),
|
||||
server_actor_ref.clone(),
|
||||
);
|
||||
(forge_name.clone(), repo_alias, actor)
|
||||
}
|
||||
|
@ -231,31 +274,43 @@ impl ServerActor {
|
|||
}
|
||||
|
||||
/// Attempts to gracefully shutdown the server before stopping the system.
|
||||
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
|
||||
self.do_send(crate::server::actor::messages::Shutdown, ctx);
|
||||
async fn abort(
|
||||
&mut self,
|
||||
ctx: &Context<'_, Self, color_eyre::Result<()>>,
|
||||
message: impl Into<String> + Send,
|
||||
) -> Result<()> {
|
||||
let message = message.into();
|
||||
error!(%message, "aborting");
|
||||
self.do_send(Shutdown, ctx).await?;
|
||||
if let Some(t) = self.shutdown_trigger.take() {
|
||||
let _ = t.send(message.into());
|
||||
} else {
|
||||
error!("{}", message.into());
|
||||
self.do_send(Shutdown, ctx);
|
||||
// System::current().stop_with_code(1);
|
||||
t.send(message)
|
||||
.await
|
||||
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context)
|
||||
async fn do_send<M>(
|
||||
&self,
|
||||
msg: M,
|
||||
ctx: &Context<'_, Self, color_eyre::Result<()>>,
|
||||
) -> Result<()>
|
||||
where
|
||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||
Self: actix::Handler<M>,
|
||||
<M as actix::Message>::Result: Send,
|
||||
M: Send + Sync + 'static + std::fmt::Debug,
|
||||
Self: kameo::message::Message<M>,
|
||||
<Self as kameo::message::Message<M>>::Reply: Send + Sync + 'static + std::fmt::Debug,
|
||||
<<Self as kameo::message::Message<M>>::Reply as kameo::Reply>::Error: std::fmt::Debug,
|
||||
{
|
||||
let log_message = format!("send: {msg:?}");
|
||||
if let Some(message_log) = &self.message_log {
|
||||
let log_message = format!("send: {msg:?}");
|
||||
if let Ok(mut log) = message_log.write() {
|
||||
log.push(log_message);
|
||||
log.push(log_message.clone());
|
||||
}
|
||||
}
|
||||
if cfg!(not(test)) {
|
||||
ctx.address().do_send(msg);
|
||||
tell!(ctx.actor_ref(), msg)
|
||||
.map_err(|e| format!("failed sending: {log_message}: {e:?}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::prelude::*;
|
||||
use kameo::actor::ActorRef;
|
||||
|
||||
use crate::alerts::{AlertsActor, History};
|
||||
|
||||
//
|
||||
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
|
||||
#[allow(clippy::expect_used)]
|
||||
kxio::fs::temp().expect("temp fs")
|
||||
|
@ -14,6 +14,10 @@ pub fn a_network() -> kxio::net::MockNet {
|
|||
kxio::net::mock()
|
||||
}
|
||||
|
||||
pub fn an_alerts_actor(net: kxio::net::Net) -> Addr<AlertsActor> {
|
||||
AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start()
|
||||
pub fn an_alerts_actor(net: kxio::net::Net) -> ActorRef<AlertsActor> {
|
||||
kameo::spawn(AlertsActor::new(
|
||||
None,
|
||||
History::new(Duration::from_millis(1)),
|
||||
net,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor};
|
||||
use git_next_core::{
|
||||
git,
|
||||
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
|
||||
};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
#[test_log::test(actix::test)]
|
||||
async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||
use kameo::actor::pubsub::PubSub;
|
||||
|
||||
use git_next_core::{
|
||||
git,
|
||||
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
server::actor::{tests::given, ReceiveAppConfig, ServerActor},
|
||||
tell,
|
||||
};
|
||||
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
|
||||
//given
|
||||
// parameters
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -22,8 +28,18 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
|||
let repo = git::repository::factory::mock();
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
|
||||
let file_update_subs = kameo::spawn(PubSub::new());
|
||||
|
||||
// sut
|
||||
let server = ServerActor::new(fs.as_real(), net.into(), alerts, repo, duration);
|
||||
let server = ServerActor::new(
|
||||
false, // ui
|
||||
fs.as_real(),
|
||||
net.into(),
|
||||
alerts,
|
||||
file_update_subs,
|
||||
repo,
|
||||
duration,
|
||||
);
|
||||
|
||||
// collaborators
|
||||
let listen = Listen::new(
|
||||
|
@ -39,13 +55,11 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
|||
let server = server.with_message_log(Some(message_log.clone()));
|
||||
|
||||
//when
|
||||
server.start().do_send(ReceiveAppConfig::new(AppConfig::new(
|
||||
listen,
|
||||
shout,
|
||||
server_storage,
|
||||
repos,
|
||||
)));
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
tell!(
|
||||
kameo::spawn(server),
|
||||
ReceiveAppConfig::new(AppConfig::new(listen, shout, server_storage, repos,))
|
||||
)?;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
|
||||
//then
|
||||
// INFO: assert that ReceiveValidServerConfig is NOT sent
|
||||
|
@ -53,4 +67,6 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
|||
assert!(message_log.read().iter().any(|log| !log
|
||||
.iter()
|
||||
.any(|line| line == "send: ReceiveValidServerConfig")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,35 +1,22 @@
|
|||
//
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::info;
|
||||
|
||||
use git_next_core::git::RepositoryFactory;
|
||||
|
||||
use crate::{root::RootActor, tell};
|
||||
|
||||
pub use actor::ServerActor;
|
||||
|
||||
pub mod actor;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix_rt::signal;
|
||||
use actor::messages::ShutdownTrigger;
|
||||
|
||||
use crate::{
|
||||
alerts::{AlertsActor, History},
|
||||
file_watcher::{watch_file, FileUpdated},
|
||||
};
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub use actor::ServerActor;
|
||||
|
||||
use git_next_core::git::RepositoryFactory;
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
use tracing::info;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::Ordering, mpsc::channel, Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
pub fn init(fs: &FileSystem) -> Result<()> {
|
||||
let file_name = "git-next-server.toml";
|
||||
let pathbuf = PathBuf::from(file_name);
|
||||
|
@ -51,8 +38,8 @@ pub fn init(fs: &FileSystem) -> Result<()> {
|
|||
#[allow(clippy::too_many_lines)]
|
||||
pub fn start(
|
||||
ui: bool,
|
||||
fs: FileSystem,
|
||||
net: Net,
|
||||
fs: &FileSystem,
|
||||
net: &Net,
|
||||
repo: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
) -> Result<()> {
|
||||
|
@ -66,111 +53,48 @@ pub fn start(
|
|||
}
|
||||
|
||||
let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
let shutdown_message_holder_exec = shutdown_message_holder.clone();
|
||||
let file_watcher_err_holder: Arc<RwLock<Option<anyhow::Error>>> = Arc::new(RwLock::new(None));
|
||||
let file_watcher_err_holder_exec = file_watcher_err_holder.clone();
|
||||
let execution = async move {
|
||||
info!("Starting Alert Dispatcher...");
|
||||
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
|
||||
let shutdown_message_holder_clone = shutdown_message_holder.clone();
|
||||
|
||||
info!("Starting Server...");
|
||||
let server =
|
||||
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
|
||||
#[allow(clippy::expect_used)]
|
||||
tokio::runtime::Runtime::new()?.block_on(async {
|
||||
let (tx_shutdown, mut rx_shutdown) = tokio::sync::mpsc::channel::<String>(1);
|
||||
|
||||
info!("Starting File Watcher...");
|
||||
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
|
||||
let fw_shutdown = match watch_file {
|
||||
Ok(fw_shutdown) => fw_shutdown,
|
||||
Err(err) => {
|
||||
// shutdown now
|
||||
server.do_send(crate::server::actor::messages::Shutdown);
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
System::current().stop();
|
||||
let _ = file_watcher_err_holder_exec
|
||||
.write()
|
||||
.map(|mut o| o.replace(err));
|
||||
return;
|
||||
let root_actor_ref = kameo::spawn(RootActor::new(
|
||||
ui,
|
||||
fs.clone(),
|
||||
net.clone(),
|
||||
sleep_duration,
|
||||
tx_shutdown,
|
||||
));
|
||||
tell!("root", root_actor_ref, crate::root::Start::new(repo)).expect("start root actor");
|
||||
|
||||
info!("Server running - Press Ctrl-C to stop...");
|
||||
tokio::select! {
|
||||
_r = tokio::signal::ctrl_c() => {
|
||||
info!("Ctrl-C received, shutting down...");
|
||||
}
|
||||
};
|
||||
|
||||
let (tx_shutdown, rx_shutdown) = channel::<String>();
|
||||
if ui {
|
||||
#[cfg(feature = "tui")]
|
||||
{
|
||||
use crate::server::actor::messages::SubscribeToUpdates;
|
||||
use crate::tui;
|
||||
|
||||
let tui_addr = tui::Tui::new(tx_shutdown.clone()).start();
|
||||
server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient()));
|
||||
server.do_send(ShutdownTrigger::new(tx_shutdown));
|
||||
server.do_send(FileUpdated); // update file after ui subscription in place
|
||||
loop {
|
||||
let _ = tui_addr.send(tui::Tick).await;
|
||||
if let Ok(message) = rx_shutdown.try_recv() {
|
||||
let _ = shutdown_message_holder_exec
|
||||
.write()
|
||||
.map(|mut o| o.replace(message));
|
||||
break;
|
||||
}
|
||||
actix_rt::time::sleep(Duration::from_millis(16)).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server.do_send(ShutdownTrigger::new(tx_shutdown.clone()));
|
||||
server.do_send(FileUpdated);
|
||||
|
||||
info!("Server running - Press Ctrl-C to stop...");
|
||||
tokio::select! {
|
||||
_r = signal::ctrl_c() => {
|
||||
info!("Ctrl-C received, shutting down...");
|
||||
}
|
||||
_x = async move {
|
||||
_x = async move {
|
||||
loop{
|
||||
if let Ok(message) = rx_shutdown.try_recv() {
|
||||
let _ = shutdown_message_holder_exec
|
||||
.write()
|
||||
.map(|mut o| o.replace(message));
|
||||
if let Some(message) = rx_shutdown.recv().await {
|
||||
info!("rx shutdown received: {message}");
|
||||
let _ = shutdown_message_holder_clone.write().await.replace(message);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
break;
|
||||
}
|
||||
actix_rt::task::yield_now().await;
|
||||
}
|
||||
} => {
|
||||
info!("signaled shutdown");
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// shutdown
|
||||
fw_shutdown.store(true, Ordering::Relaxed);
|
||||
server.do_send(crate::server::actor::messages::Shutdown);
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
System::current().stop();
|
||||
};
|
||||
|
||||
let system = System::new();
|
||||
Arbiter::current().spawn(execution);
|
||||
system.run()?;
|
||||
|
||||
// check for error from server thread
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if let Some(err) = &*shutdown_message_holder.read().unwrap() {
|
||||
#[cfg(feature = "tui")]
|
||||
if ui {
|
||||
ratatui::restore();
|
||||
}
|
||||
if !err.is_empty() {
|
||||
return Err(color_eyre::eyre::eyre!(format!("{err}")));
|
||||
}
|
||||
#[cfg(feature = "tui")]
|
||||
if ui {
|
||||
ratatui::restore();
|
||||
}
|
||||
|
||||
// check for error from file watcher thread
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if let Some(err) = &*file_watcher_err_holder.read().unwrap() {
|
||||
#[cfg(feature = "tui")]
|
||||
if ui {
|
||||
ratatui::restore();
|
||||
}
|
||||
return Err(color_eyre::eyre::eyre!(format!("{err}")));
|
||||
if let Some(ref message) = *shutdown_message_holder.blocking_write() {
|
||||
info!(%message, "shutdown");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -38,44 +38,3 @@ mod init {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
mod file_watcher {
|
||||
use std::{sync::atomic::Ordering, time::Duration};
|
||||
|
||||
use actix::{Actor, Context, Handler};
|
||||
use rstest::*;
|
||||
|
||||
use crate::file_watcher::{self, FileUpdated};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
#[rstest]
|
||||
#[actix::test]
|
||||
#[timeout(Duration::from_millis(80))]
|
||||
async fn should_not_block_calling_thread() -> TestResult {
|
||||
let fs = kxio::fs::temp()?;
|
||||
let path = fs.base().join("file");
|
||||
fs.file(&path).write("foo")?;
|
||||
|
||||
let listener = Listener;
|
||||
let l_addr = listener.start();
|
||||
let recipient = l_addr.recipient();
|
||||
|
||||
let fw_shutdown = file_watcher::watch_file(path, recipient)?;
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
fw_shutdown.store(true, Ordering::Relaxed);
|
||||
|
||||
Ok(()) // was not blocked
|
||||
}
|
||||
|
||||
struct Listener;
|
||||
impl Actor for Listener {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
impl Handler<FileUpdated> for Listener {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, _msg: FileUpdated, _ctx: &mut Self::Context) -> Self::Result {
|
||||
// todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//
|
||||
use actix::Handler;
|
||||
use kameo::message::{Context, Message};
|
||||
use ratatui::style::Color;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
server::actor::messages::{RepoUpdate, ServerUpdate},
|
||||
|
@ -12,10 +13,14 @@ static PREP: Color = Color::Gray;
|
|||
static ACTING: Color = Color::LightBlue;
|
||||
static WARN: Color = Color::Red;
|
||||
|
||||
impl Handler<ServerUpdate> for Tui {
|
||||
type Result = ();
|
||||
impl Message<ServerUpdate> for Tui {
|
||||
type Reply = ();
|
||||
|
||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: ServerUpdate,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
self.state.tap();
|
||||
match msg {
|
||||
ServerUpdate::AppConfigLoaded { app_config } => {
|
||||
|
@ -29,76 +34,82 @@ impl Handler<ServerUpdate> for Tui {
|
|||
} => {
|
||||
if let ServerState::Configured { forges } = &mut self.state.mode {
|
||||
let Some(forge_state) = forges.get_mut(&forge_alias) else {
|
||||
debug!("ServerState::Configured: no forge state available");
|
||||
return;
|
||||
};
|
||||
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
|
||||
debug!("ServerState::Configured: no repo state available");
|
||||
return;
|
||||
};
|
||||
repo_state.clear_alert();
|
||||
match repo_update {
|
||||
RepoUpdate::Branches { branches } => {
|
||||
repo_state.update_branches(branches);
|
||||
}
|
||||
RepoUpdate::Log { log } => {
|
||||
repo_state.update_log(log);
|
||||
}
|
||||
RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING),
|
||||
RepoUpdate::Okay { main, next, dev } => {
|
||||
repo_state.clear_alert();
|
||||
repo_state.update_message("okay", OKAY);
|
||||
*repo_state = repo_state.clone().ready(main, next, dev);
|
||||
}
|
||||
RepoUpdate::Alert { alert } => {
|
||||
repo_state.alert(alert);
|
||||
}
|
||||
RepoUpdate::CheckingCI => {
|
||||
repo_state.update_message("Checking CI status", ACTING);
|
||||
}
|
||||
RepoUpdate::AdvancingNext { commit, force: _ } => {
|
||||
repo_state
|
||||
.update_message(format!("advancing next to {commit}"), ACTING);
|
||||
}
|
||||
RepoUpdate::NextUpdated => {
|
||||
repo_state.update_message("next updated - pause while CI starts", OKAY);
|
||||
}
|
||||
RepoUpdate::AdvancingMain { commit } => {
|
||||
repo_state
|
||||
.update_message(format!("advancing main to {commit}"), ACTING);
|
||||
}
|
||||
RepoUpdate::MainUpdated => {
|
||||
repo_state.update_message("main updated", OKAY);
|
||||
}
|
||||
RepoUpdate::Opening => {
|
||||
repo_state.update_message("opening...", PREP);
|
||||
}
|
||||
RepoUpdate::Opened => {
|
||||
repo_state.update_message("opened", PREP);
|
||||
}
|
||||
RepoUpdate::LoadingConfigFromRepo => {
|
||||
repo_state.update_message("loading config from repo...", PREP);
|
||||
}
|
||||
RepoUpdate::ReceiveCIStatus { status } => {
|
||||
repo_state.update_message(format!("ci status: {status:?}"), WARN);
|
||||
}
|
||||
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
|
||||
repo_state.update_message("loaded config from repo", PREP);
|
||||
}
|
||||
RepoUpdate::RegisteringWebhook => {
|
||||
repo_state.update_message("registering webhook...", PREP);
|
||||
}
|
||||
RepoUpdate::UnregisteringWebhook => {
|
||||
repo_state.update_message("unregistering webhook...", PREP);
|
||||
}
|
||||
RepoUpdate::WebhookReceived { branch, push: _ } => {
|
||||
repo_state
|
||||
.update_message(format!("webhook update: {branch:?}"), ACTING);
|
||||
}
|
||||
RepoUpdate::RegisteredWebhook => {
|
||||
repo_state.update_message("registered webhook", PREP);
|
||||
}
|
||||
}
|
||||
debug!(?repo_update, "ServerState::Configured: RepoUpdate");
|
||||
handle_repo_update(repo_update, repo_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_repo_update(repo_update: RepoUpdate, repo_state: &mut crate::tui::actor::RepoState) {
|
||||
match repo_update {
|
||||
RepoUpdate::Branches { branches } => {
|
||||
repo_state.update_branches(branches);
|
||||
}
|
||||
RepoUpdate::Log { log } => {
|
||||
repo_state.update_log(log);
|
||||
}
|
||||
RepoUpdate::ValidateRepo => {
|
||||
repo_state.update_message("polling...", ACTING);
|
||||
}
|
||||
RepoUpdate::Okay { main, next, dev } => {
|
||||
repo_state.clear_alert();
|
||||
repo_state.update_message("okay", OKAY);
|
||||
*repo_state = repo_state.clone().ready(main, next, dev);
|
||||
}
|
||||
RepoUpdate::Alert { alert } => {
|
||||
repo_state.alert(alert);
|
||||
}
|
||||
RepoUpdate::CheckingCI => {
|
||||
repo_state.update_message("Checking CI status", ACTING);
|
||||
}
|
||||
RepoUpdate::AdvancingNext { commit, force: _ } => {
|
||||
repo_state.update_message(format!("advancing next to {commit}"), ACTING);
|
||||
}
|
||||
RepoUpdate::NextUpdated => {
|
||||
repo_state.update_message("next updated - pause while CI starts", OKAY);
|
||||
}
|
||||
RepoUpdate::AdvancingMain { commit } => {
|
||||
repo_state.update_message(format!("advancing main to {commit}"), ACTING);
|
||||
}
|
||||
RepoUpdate::MainUpdated => {
|
||||
repo_state.update_message("main updated", OKAY);
|
||||
}
|
||||
RepoUpdate::Opening => {
|
||||
repo_state.update_message("opening...", PREP);
|
||||
}
|
||||
RepoUpdate::Opened => {
|
||||
repo_state.update_message("opened", PREP);
|
||||
}
|
||||
RepoUpdate::LoadingConfigFromRepo => {
|
||||
repo_state.update_message("loading config from repo...", PREP);
|
||||
}
|
||||
RepoUpdate::ReceiveCIStatus { status } => {
|
||||
repo_state.update_message(format!("ci status: {status:?}"), WARN);
|
||||
}
|
||||
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
|
||||
repo_state.update_message("loaded config from repo", PREP);
|
||||
}
|
||||
RepoUpdate::RegisteringWebhook => {
|
||||
repo_state.update_message("registering webhook...", PREP);
|
||||
}
|
||||
RepoUpdate::UnregisteringWebhook => {
|
||||
repo_state.update_message("unregistering webhook...", PREP);
|
||||
}
|
||||
RepoUpdate::WebhookReceived { branch, push: _ } => {
|
||||
repo_state.update_message(format!("webhook update: {branch:?}"), ACTING);
|
||||
}
|
||||
RepoUpdate::RegisteredWebhook => {
|
||||
repo_state.update_message("registered webhook", PREP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
//
|
||||
use actix::Handler;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::tui::actor::{messages::Tick, Tui};
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
impl Handler<Tick> for Tui {
|
||||
type Result = std::io::Result<()>;
|
||||
use crate::{
|
||||
tell,
|
||||
tui::actor::{messages::Tick, Tui},
|
||||
};
|
||||
|
||||
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
|
||||
impl Message<Tick> for Tui {
|
||||
type Reply = Result<()>;
|
||||
|
||||
async fn handle(&mut self, _msg: Tick, ctx: Context<'_, Self, Self::Reply>) -> Self::Reply {
|
||||
self.state.tap();
|
||||
self.draw()?;
|
||||
self.handle_input(ctx)?;
|
||||
self.handle_input(&ctx.actor_ref())?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(16)).await;
|
||||
tell!(ctx.actor_ref(), Tick)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,61 +6,85 @@ mod model;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use actix::{Actor, ActorContext as _, Context};
|
||||
|
||||
pub use model::*;
|
||||
|
||||
use color_eyre::Result;
|
||||
use kameo::{
|
||||
actor::{ActorRef, WeakActorRef},
|
||||
error::{ActorStopReason, BoxError, PanicError},
|
||||
mailbox::unbounded::UnboundedMailbox,
|
||||
Actor,
|
||||
};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, KeyCode, KeyEventKind},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use tui_scrollview::ScrollViewState;
|
||||
|
||||
pub use model::*;
|
||||
|
||||
use crate::tell;
|
||||
|
||||
use super::Tick;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Tui {
|
||||
terminal: Option<DefaultTerminal>,
|
||||
signal_shutdown: Sender<String>,
|
||||
pub state: State,
|
||||
scroll_view_state: ScrollViewState,
|
||||
}
|
||||
impl Actor for Tui {
|
||||
type Context = Context<Self>;
|
||||
fn started(&mut self, _ctx: &mut Self::Context) {
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
|
||||
async fn on_start(&mut self, actor_ref: ActorRef<Self>) -> Result<(), BoxError> {
|
||||
self.terminal.replace(ratatui::init());
|
||||
tell!(actor_ref, Tick)?;
|
||||
Ok(())
|
||||
}
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
|
||||
async fn on_stop(
|
||||
&mut self,
|
||||
_actor_ref: WeakActorRef<Self>,
|
||||
_reason: ActorStopReason,
|
||||
) -> Result<(), BoxError> {
|
||||
self.terminal.take();
|
||||
ratatui::restore();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_panic(
|
||||
&mut self,
|
||||
_actor_ref: WeakActorRef<Self>,
|
||||
err: PanicError,
|
||||
) -> Result<Option<ActorStopReason>, BoxError> {
|
||||
self.terminal.take();
|
||||
ratatui::restore();
|
||||
Ok(Some(ActorStopReason::Panicked(err)))
|
||||
}
|
||||
}
|
||||
impl Tui {
|
||||
pub fn new(signal_shutdown: Sender<String>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
terminal: None,
|
||||
signal_shutdown,
|
||||
state: State::initial(),
|
||||
scroll_view_state: ScrollViewState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> std::io::Result<()> {
|
||||
let t = self.terminal.take();
|
||||
let scroll_view_state = &mut self.scroll_view_state;
|
||||
let state = &self.state;
|
||||
if let Some(mut terminal) = t {
|
||||
if let Some(terminal) = &mut self.terminal {
|
||||
terminal.draw(|frame| {
|
||||
frame.render_stateful_widget(state, frame.area(), scroll_view_state);
|
||||
frame.render_stateful_widget(
|
||||
&self.state,
|
||||
frame.area(),
|
||||
&mut self.scroll_view_state,
|
||||
);
|
||||
})?;
|
||||
self.terminal = Some(terminal);
|
||||
} else {
|
||||
eprintln!("No terminal setup");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
|
||||
fn handle_input(&mut self, actor_tui: &ActorRef<Self>) -> Result<()> {
|
||||
if event::poll(std::time::Duration::from_millis(16))? {
|
||||
let event::Event::Key(key) = event::read()? else {
|
||||
return Ok(());
|
||||
|
@ -70,10 +94,7 @@ impl Tui {
|
|||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.stop();
|
||||
if let Err(err) = self.signal_shutdown.send(String::new()) {
|
||||
tracing::error!(?err, "Failed to signal shutdown");
|
||||
}
|
||||
actor_tui.kill();
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//
|
||||
use std::{collections::BTreeMap, fmt::Display, time::Instant};
|
||||
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
prelude::{Buffer, Rect},
|
||||
|
@ -7,15 +9,13 @@ use ratatui::{
|
|||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph, StatefulWidget, Widget},
|
||||
};
|
||||
use tracing::info;
|
||||
use tui_scrollview::ScrollViewState;
|
||||
|
||||
use git_next_core::{
|
||||
git::{self, graph::Log, Commit},
|
||||
ForgeAlias, RepoAlias, RepoBranches,
|
||||
};
|
||||
use tracing::info;
|
||||
use tui_scrollview::ScrollViewState;
|
||||
|
||||
use std::{collections::BTreeMap, fmt::Display, time::Instant};
|
||||
|
||||
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
mod shutdown_webhook;
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use crate::webhook::{messages::ShutdownWebhook, WebhookActor};
|
||||
|
||||
impl Handler<ShutdownWebhook> for WebhookActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, _msg: ShutdownWebhook, ctx: &mut Self::Context) -> Self::Result {
|
||||
self.spawn_handle.take();
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
//
|
||||
use git_next_core::message;
|
||||
|
||||
message!(ShutdownWebhook, "Request to shutdown the Webhook actor");
|
|
@ -1,43 +1,56 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
mod handlers;
|
||||
pub mod messages;
|
||||
pub mod router;
|
||||
mod server;
|
||||
|
||||
use crate::repo::messages::WebhookNotification;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use tracing::Instrument;
|
||||
use color_eyre::Result;
|
||||
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
|
||||
use router::WebhookRouterActor;
|
||||
|
||||
use crate::{
|
||||
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start,
|
||||
default_on_actor_stop,
|
||||
};
|
||||
|
||||
pub mod router;
|
||||
mod server;
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(Debug)]
|
||||
pub struct WebhookActor {
|
||||
socket_addr: SocketAddr,
|
||||
span: tracing::Span,
|
||||
spawn_handle: Option<actix::SpawnHandle>,
|
||||
message_receiver: Recipient<WebhookNotification>,
|
||||
message_receiver: ActorRef<WebhookRouterActor>,
|
||||
}
|
||||
impl WebhookActor {
|
||||
pub fn new(socket_addr: SocketAddr, message_receiver: Recipient<WebhookNotification>) -> Self {
|
||||
let span = tracing::info_span!("WebhookActor");
|
||||
pub const fn new(
|
||||
socket_addr: SocketAddr,
|
||||
message_receiver: ActorRef<WebhookRouterActor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
socket_addr,
|
||||
span,
|
||||
message_receiver,
|
||||
spawn_handle: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for WebhookActor {
|
||||
type Context = actix::Context<Self>;
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
let _gaurd = self.span.enter();
|
||||
let address: Recipient<WebhookNotification> = self.message_receiver.clone();
|
||||
let server = server::start(self.socket_addr, address);
|
||||
let spawn_handle = ctx.spawn(server.in_current_span().into_actor(self));
|
||||
self.spawn_handle.replace(spawn_handle);
|
||||
type Mailbox = UnboundedMailbox<Self>;
|
||||
|
||||
default_on_actor_start!(this, actor_ref);
|
||||
default_on_actor_panic!(this, actor_ref, err);
|
||||
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||
default_on_actor_stop!(this, actor_ref, reason);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Start;
|
||||
impl Message<Start> for WebhookActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_msg: Start,
|
||||
_ctx: kameo::message::Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
server::start(self.socket_addr, self.message_receiver.clone()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use derive_more::Constructor;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::repo::messages::WebhookNotification;
|
||||
use derive_more::Constructor;
|
||||
use kameo::{
|
||||
actor::ActorRef,
|
||||
message::{Context, Message},
|
||||
Actor,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use git_next_core::{ForgeAlias, RepoAlias};
|
||||
|
||||
use crate::{
|
||||
repo::{messages::WebhookNotification, RepoActor},
|
||||
tell,
|
||||
};
|
||||
|
||||
#[derive(Actor)]
|
||||
pub struct WebhookRouterActor {
|
||||
span: tracing::Span,
|
||||
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, Recipient<WebhookNotification>>>,
|
||||
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, ActorRef<RepoActor>>>,
|
||||
}
|
||||
impl Default for WebhookRouterActor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl WebhookRouterActor {
|
||||
pub fn new() -> Self {
|
||||
let span = tracing::info_span!("WebhookRouter");
|
||||
Self {
|
||||
span,
|
||||
|
@ -28,43 +30,45 @@ impl WebhookRouterActor {
|
|||
}
|
||||
}
|
||||
}
|
||||
impl Actor for WebhookRouterActor {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
impl Message<WebhookNotification> for WebhookRouterActor {
|
||||
type Reply = color_eyre::Result<()>;
|
||||
|
||||
impl Handler<WebhookNotification> for WebhookRouterActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: WebhookNotification, _ctx: &mut Self::Context) -> Self::Result {
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: WebhookNotification,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let _gaurd = self.span.enter();
|
||||
let forge_alias = msg.forge_alias();
|
||||
let repo_alias = msg.repo_alias();
|
||||
debug!(forge = %forge_alias, repo = %repo_alias, "Router...");
|
||||
let Some(forge_repos) = self.recipients.get(forge_alias) else {
|
||||
warn!(forge = %forge_alias, "No forge repos found");
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let Some(recipient) = forge_repos.get(repo_alias) else {
|
||||
debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found");
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
recipient.do_send(msg);
|
||||
tell!(recipient, msg)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message, Constructor)]
|
||||
#[rtype(result = "()")]
|
||||
#[derive(Constructor, Debug)]
|
||||
pub struct AddWebhookRecipient {
|
||||
pub forge_alias: ForgeAlias,
|
||||
pub repo_alias: RepoAlias,
|
||||
pub recipient: Recipient<WebhookNotification>,
|
||||
pub recipient: ActorRef<RepoActor>,
|
||||
}
|
||||
impl Handler<AddWebhookRecipient> for WebhookRouterActor {
|
||||
type Result = ();
|
||||
impl Message<AddWebhookRecipient> for WebhookRouterActor {
|
||||
type Reply = ();
|
||||
|
||||
fn handle(&mut self, msg: AddWebhookRecipient, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let _gaurd = self.span.enter();
|
||||
info!(forge = %msg.forge_alias, repo = %msg.repo_alias, "Register Recipient");
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: AddWebhookRecipient,
|
||||
_ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
if !self.recipients.contains_key(&msg.forge_alias) {
|
||||
self.recipients
|
||||
.insert(msg.forge_alias.clone(), BTreeMap::new());
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use std::{collections::BTreeMap, net::SocketAddr};
|
||||
|
||||
use color_eyre::Result;
|
||||
use kameo::actor::ActorRef;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::repo::messages::WebhookNotification;
|
||||
|
||||
use git_next_core::{webhook, ForgeAlias, ForgeNotification, RepoAlias};
|
||||
|
||||
pub async fn start(
|
||||
socket_addr: SocketAddr,
|
||||
address: actix::prelude::Recipient<WebhookNotification>,
|
||||
) {
|
||||
use crate::{repo::messages::WebhookNotification, tell};
|
||||
|
||||
use super::router::WebhookRouterActor;
|
||||
|
||||
pub async fn start(socket_addr: SocketAddr, address: ActorRef<WebhookRouterActor>) -> Result<()> {
|
||||
// start webhook server
|
||||
use warp::Filter;
|
||||
// Define the Warp route to handle incoming HTTP requests
|
||||
|
@ -23,37 +22,28 @@ pub async fn start(
|
|||
.and(warp::header::headers_cloned())
|
||||
.and(warp::body::bytes())
|
||||
.and_then(
|
||||
|recipient: Recipient<WebhookNotification>,
|
||||
|recipient: ActorRef<WebhookRouterActor>,
|
||||
forge_alias: String,
|
||||
repo_alias: String,
|
||||
// query: String,
|
||||
headers: warp::http::HeaderMap,
|
||||
body: bytes::Bytes| async move {
|
||||
info!("POST received");
|
||||
let forge_alias = ForgeAlias::new(forge_alias);
|
||||
let repo_alias = RepoAlias::new(repo_alias);
|
||||
let bytes = body.to_vec();
|
||||
let body = webhook::forge_notification::Body::new(
|
||||
String::from_utf8_lossy(&bytes).to_string(),
|
||||
);
|
||||
let headers = headers
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| {
|
||||
k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
|
||||
})
|
||||
.collect::<BTreeMap<String, String>>();
|
||||
let message = WebhookNotification::new(ForgeNotification::new(
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
headers,
|
||||
body,
|
||||
let msg = WebhookNotification::new(ForgeNotification::new(
|
||||
ForgeAlias::new(forge_alias),
|
||||
RepoAlias::new(repo_alias),
|
||||
headers
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| {
|
||||
k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
|
||||
})
|
||||
.collect::<BTreeMap<String, String>>(),
|
||||
webhook::forge_notification::Body::new(
|
||||
String::from_utf8_lossy(&body).to_string(),
|
||||
),
|
||||
));
|
||||
recipient
|
||||
.try_send(message)
|
||||
.map(|()| {
|
||||
info!("Message sent ok");
|
||||
warp::reply::with_status("OK", warp::http::StatusCode::OK)
|
||||
})
|
||||
tell!(recipient, msg)
|
||||
.map(|()| warp::reply::with_status("OK", warp::http::StatusCode::OK))
|
||||
.map_err(|e| {
|
||||
warn!("Unknown error: {:?}", e);
|
||||
warp::reject()
|
||||
|
@ -63,5 +53,9 @@ pub async fn start(
|
|||
|
||||
// Start the server
|
||||
info!("Starting webhook server: {}", socket_addr);
|
||||
warp::serve(route).run(socket_addr).await;
|
||||
let (socket, f) = warp::serve(route).try_bind_ephemeral(socket_addr)?;
|
||||
info!("Server bound to {socket}");
|
||||
f.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -27,9 +27,6 @@ tracing = { workspace = true }
|
|||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# Actors
|
||||
actix = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
|
|
@ -352,3 +352,13 @@ impl SmtpConfig {
|
|||
&self.hostname
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const fn is_sendable<T: Send>() {}
|
||||
|
||||
#[test]
|
||||
const fn normal() {
|
||||
is_sendable::<super::AppConfig>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,14 +50,11 @@ pub fn open(
|
|||
|
||||
let open_repository = if repo_details.gitdir.exists() {
|
||||
info!("Local copy found - opening...");
|
||||
let repo = repository_factory.open(repo_details)?;
|
||||
repo.fetch()?;
|
||||
repo
|
||||
repository_factory.open(repo_details)?
|
||||
} else {
|
||||
info!("Local copy not found - cloning...");
|
||||
repository_factory.git_clone(repo_details)?
|
||||
};
|
||||
info!("Validating...");
|
||||
validate_default_remotes(&*open_repository, repo_details)
|
||||
.map_err(|e| Error::Validation(s!(e)))?;
|
||||
Ok(open_repository)
|
||||
|
|
|
@ -63,7 +63,7 @@ pub(crate) fn test(
|
|||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[mockall::automock]
|
||||
pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
|
||||
pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||
/// Creates a clone of the `OpenRepositoryLike`.
|
||||
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
||||
|
||||
|
|
|
@ -4,8 +4,6 @@ use crate::{
|
|||
s, RemoteUrl,
|
||||
};
|
||||
|
||||
use tracing::info;
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn validate_default_remotes(
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
|
@ -22,7 +20,6 @@ pub fn validate_default_remotes(
|
|||
"Unable to build forge url"
|
||||
)));
|
||||
};
|
||||
info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
|
||||
if !remote_url.matches(&push_remote) {
|
||||
return Err(Error::MismatchDefaultPushRemote {
|
||||
found: push_remote,
|
||||
|
|
|
@ -2,26 +2,14 @@
|
|||
macro_rules! message {
|
||||
($name:ident, $value:ty, $docs:literal) => {
|
||||
git_next_core::newtype!($name, $value, $docs);
|
||||
impl actix::prelude::Message for $name {
|
||||
type Result = ();
|
||||
}
|
||||
};
|
||||
($name:ident, $docs:literal) => {
|
||||
git_next_core::newtype!($name, $docs);
|
||||
impl actix::prelude::Message for $name {
|
||||
type Result = ();
|
||||
}
|
||||
};
|
||||
($name:ident, $value:ty => $result:ty, $docs:literal) => {
|
||||
git_next_core::newtype!($name, $value, $docs);
|
||||
impl actix::prelude::Message for $name {
|
||||
type Result = $result;
|
||||
}
|
||||
};
|
||||
($name:ident => $result:ty, $docs:literal) => {
|
||||
git_next_core::newtype!($name, $docs);
|
||||
impl actix::prelude::Message for $name {
|
||||
type Result = $result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use git_next_core::git::forge::webhook::Error;
|
||||
//
|
||||
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
|
||||
|
||||
|
@ -9,14 +10,14 @@ use tracing::{info, instrument, warn};
|
|||
use crate::webhook;
|
||||
use crate::webhook::Hook;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
#[instrument(skip_all, fields(forge = %repo_details.forge.forge_alias(), repo = %repo_details.repo_alias))]
|
||||
pub async fn register(
|
||||
repo_details: &git::RepoDetails,
|
||||
repo_listen_url: &RepoListenUrl,
|
||||
net: &Net,
|
||||
) -> git::forge::webhook::Result<RegisteredWebhook> {
|
||||
let Some(repo_config) = repo_details.repo_config.clone() else {
|
||||
return Err(git::forge::webhook::Error::NoRepoConfig);
|
||||
return Err(Error::NoRepoConfig);
|
||||
};
|
||||
|
||||
// remove any lingering webhooks for the same URL
|
||||
|
@ -51,17 +52,17 @@ pub async fn register(
|
|||
let Ok(hook) = response.json::<Hook>().await else {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
// request response is Json so response_body never returns None
|
||||
return Err(git::forge::webhook::Error::NetworkResponseEmpty);
|
||||
return Err(Error::NetworkResponseEmpty);
|
||||
};
|
||||
info!(webhook_id = %hook.id, "Webhook registered");
|
||||
info!(webhook_id = %hook.id, "ok");
|
||||
Ok(RegisteredWebhook::new(
|
||||
WebhookId::new(format!("{}", hook.id)),
|
||||
authorisation,
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to register webhook");
|
||||
Err(git::forge::webhook::Error::FailedToRegister(e.to_string()))
|
||||
warn!(?e, "failed");
|
||||
Err(Error::FailedToRegister(e.to_string()))
|
||||
}
|
||||
}
|
||||
// Ok(())
|
||||
|
|
1
justfile
1
justfile
|
@ -3,6 +3,7 @@ build:
|
|||
set -e
|
||||
cargo fmt
|
||||
cargo fmt --check
|
||||
cargo machete
|
||||
cargo hack clippy
|
||||
cargo hack build
|
||||
cargo hack test
|
||||
|
|
3
quickfix-564
Normal file
3
quickfix-564
Normal file
|
@ -0,0 +1,3 @@
|
|||
crates/cli/src/server/actor/mod.rs ┃74┃pub fn new(
|
||||
crates/cli/src/server/actor/tests/receive_app_config.rs ┃31┃let server = ServerActor::new(
|
||||
crates/cli/src/server/mod.rs ┃83┃let server = kameo::spqawn(ServerActor::new(
|
Loading…
Reference in a new issue