feat: post webhook notifications to user
All checks were successful
Rust / build (push) Successful in 2m56s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
All checks were successful
Rust / build (push) Successful in 2m56s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
Closes kemitix/git-next#91
This commit is contained in:
parent
288c20c24b
commit
9e12f5eb5d
16 changed files with 183 additions and 18 deletions
|
@ -78,6 +78,8 @@ git-conventional = "0.12"
|
||||||
bytes = "1.6"
|
bytes = "1.6"
|
||||||
ulid = "1.1"
|
ulid = "1.1"
|
||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
|
time = "0.3"
|
||||||
|
standardwebhooks = "1.0"
|
||||||
|
|
||||||
# boilerplate
|
# boilerplate
|
||||||
derive_more = { version = "1.0.0-beta.6", features = [
|
derive_more = { version = "1.0.0-beta.6", features = [
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
crate::newtype!(BranchName: String, derive_more::Display, Default: "The name of a Git branch");
|
use serde::Serialize;
|
||||||
|
|
||||||
|
crate::newtype!(BranchName: String, derive_more::Display, Default, Serialize: "The name of a Git branch");
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
crate::newtype!(ForgeAlias: String, Hash, PartialOrd, Ord, derive_more::Display, Default: "The name of a Forge to connect to");
|
use serde::Serialize;
|
||||||
|
|
||||||
|
crate::newtype!(ForgeAlias: String, Hash, PartialOrd, Ord, derive_more::Display, Default, Serialize: "The name of a Forge to connect to");
|
||||||
impl From<&ForgeAlias> for std::path::PathBuf {
|
impl From<&ForgeAlias> for std::path::PathBuf {
|
||||||
fn from(value: &ForgeAlias) -> Self {
|
fn from(value: &ForgeAlias) -> Self {
|
||||||
Self::from(&value.0)
|
Self::from(&value.0)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::newtype;
|
use crate::newtype;
|
||||||
|
|
||||||
newtype!(RepoAlias: String, Display, Default, PartialOrd, Ord: r#"The alias of a repo.
|
newtype!(RepoAlias: String, Display, Default, PartialOrd, Ord, Serialize: r#"The alias of a repo.
|
||||||
|
|
||||||
This is the alias for the repo within `git-next-server.toml`."#);
|
This is the alias for the repo within `git-next-server.toml`."#);
|
||||||
|
|
|
@ -199,7 +199,7 @@ impl Notification {
|
||||||
webhook: None,
|
webhook: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn webhook(webhook: OutboundWebhook) -> Self {
|
pub const fn new_webhook(webhook: OutboundWebhook) -> Self {
|
||||||
Self {
|
Self {
|
||||||
r#type: NotificationType::Webhook,
|
r#type: NotificationType::Webhook,
|
||||||
webhook: Some(webhook),
|
webhook: Some(webhook),
|
||||||
|
@ -210,6 +210,10 @@ impl Notification {
|
||||||
self.r#type
|
self.r#type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn webhook(&self) -> Option<&OutboundWebhook> {
|
||||||
|
self.webhook.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn webhook_url(&self) -> Option<String> {
|
pub fn webhook_url(&self) -> Option<String> {
|
||||||
self.webhook.clone().map(|x| x.url)
|
self.webhook.clone().map(|x| x.url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -753,7 +753,7 @@ mod given {
|
||||||
ServerStorage::new(a_name().into())
|
ServerStorage::new(a_name().into())
|
||||||
}
|
}
|
||||||
pub fn a_notification_config() -> Notification {
|
pub fn a_notification_config() -> Notification {
|
||||||
Notification::webhook(an_outbound_webhook())
|
Notification::new_webhook(an_outbound_webhook())
|
||||||
}
|
}
|
||||||
pub fn some_forge_configs() -> BTreeMap<String, ForgeConfig> {
|
pub fn some_forge_configs() -> BTreeMap<String, ForgeConfig> {
|
||||||
[(a_name(), a_forge_config())].into()
|
[(a_name(), a_forge_config())].into()
|
||||||
|
|
|
@ -25,11 +25,13 @@ async-trait = { workspace = true }
|
||||||
# fs/network
|
# fs/network
|
||||||
kxio = { workspace = true }
|
kxio = { workspace = true }
|
||||||
|
|
||||||
# # TOML parsing
|
# TOML parsing
|
||||||
# serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
# toml = { workspace = true }
|
# toml = { workspace = true }
|
||||||
|
|
||||||
|
# webhooks - user notification
|
||||||
|
serde = { workspace = true }
|
||||||
|
|
||||||
# Secrets and Password
|
# Secrets and Password
|
||||||
secrecy = { workspace = true }
|
secrecy = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ use config::newtype;
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
//
|
//
|
||||||
use git_next_config as config;
|
use git_next_config as config;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone,
|
||||||
|
@ -13,6 +14,7 @@ use git_next_config as config;
|
||||||
Ord,
|
Ord,
|
||||||
derive_more::Constructor,
|
derive_more::Constructor,
|
||||||
derive_more::Display,
|
derive_more::Display,
|
||||||
|
Serialize,
|
||||||
)]
|
)]
|
||||||
#[display("{}", sha)]
|
#[display("{}", sha)]
|
||||||
pub struct Commit {
|
pub struct Commit {
|
||||||
|
@ -37,8 +39,8 @@ impl From<config::webhook::Push> for Commit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newtype!(Sha: String, Display, Hash,PartialOrd, Ord: "The unique SHA for a git commit.");
|
newtype!(Sha: String, Display, Hash,PartialOrd, Ord, Serialize: "The unique SHA for a git commit.");
|
||||||
newtype!(Message: String, Hash, PartialOrd, Ord: "The commit message for a git commit.");
|
newtype!(Message: String, Display, Hash, PartialOrd, Ord, Serialize: "The commit message for a git commit.");
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Histories {
|
pub struct Histories {
|
||||||
|
|
|
@ -43,6 +43,7 @@ git-conventional = { workspace = true }
|
||||||
# Webhooks
|
# Webhooks
|
||||||
bytes = { workspace = true }
|
bytes = { workspace = true }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
|
time = { workspace = true }
|
||||||
|
|
||||||
# boilerplate
|
# boilerplate
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod branch;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
mod load;
|
mod load;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
mod notifications;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
72
crates/repo-actor/src/notifications.rs
Normal file
72
crates/repo-actor/src/notifications.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use derive_more::Deref as _;
|
||||||
|
use git_next_git::UserNotification;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::messages::NotifyUser;
|
||||||
|
|
||||||
|
impl NotifyUser {
|
||||||
|
pub fn as_json(self, timestamp: time::OffsetDateTime) -> serde_json::Value {
|
||||||
|
let timestamp = timestamp.unix_timestamp().to_string();
|
||||||
|
match self.deref() {
|
||||||
|
UserNotification::CICheckFailed {
|
||||||
|
forge_alias,
|
||||||
|
repo_alias,
|
||||||
|
commit,
|
||||||
|
} => json!({
|
||||||
|
"type": "cicheck.failed",
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"data": {
|
||||||
|
"forge_alias": forge_alias,
|
||||||
|
"repo_alias": repo_alias,
|
||||||
|
"commit": {
|
||||||
|
"sha": commit.sha(),
|
||||||
|
"message": commit.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
UserNotification::RepoConfigLoadFailure {
|
||||||
|
forge_alias,
|
||||||
|
repo_alias,
|
||||||
|
reason,
|
||||||
|
} => json!({
|
||||||
|
"type": "config.load.failed",
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"data": {
|
||||||
|
"forge_alias": forge_alias,
|
||||||
|
"repo_alias": repo_alias,
|
||||||
|
"reason": reason
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
UserNotification::WebhookRegistration {
|
||||||
|
forge_alias,
|
||||||
|
repo_alias,
|
||||||
|
reason,
|
||||||
|
} => json!({
|
||||||
|
"type": "webhook.registration.failed",
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"data": {
|
||||||
|
"forge_alias": forge_alias,
|
||||||
|
"repo_alias": repo_alias,
|
||||||
|
"reason": reason
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
UserNotification::DevNotBasedOnMain {
|
||||||
|
forge_alias,
|
||||||
|
repo_alias,
|
||||||
|
dev_branch,
|
||||||
|
main_branch,
|
||||||
|
} => json!({
|
||||||
|
"type": "branch.dev.not-on-main",
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"data": {
|
||||||
|
"forge_alias": forge_alias,
|
||||||
|
"repo_alias": repo_alias,
|
||||||
|
"branches": {
|
||||||
|
"dev": dev_branch,
|
||||||
|
"main": main_branch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,14 @@ derive-with = { workspace = true }
|
||||||
# Actors
|
# Actors
|
||||||
actix = { workspace = true }
|
actix = { workspace = true }
|
||||||
|
|
||||||
|
# Webhooks
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
ulid = { workspace = true }
|
||||||
|
time = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
standardwebhooks = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Testing
|
# Testing
|
||||||
# assert2 = { workspace = true }
|
# assert2 = { workspace = true }
|
||||||
|
|
|
@ -1,14 +1,81 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
use git_next_config::server::{Notification, NotificationType};
|
||||||
use git_next_repo_actor::messages::NotifyUser;
|
use git_next_repo_actor::messages::NotifyUser;
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
use standardwebhooks::Webhook;
|
||||||
|
use tracing::Instrument;
|
||||||
|
|
||||||
use crate::ServerActor;
|
use crate::ServerActor;
|
||||||
|
|
||||||
impl Handler<NotifyUser> for ServerActor {
|
impl Handler<NotifyUser> for ServerActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
fn handle(&mut self, _msg: NotifyUser, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
|
||||||
// TODO: (#95) should notify user
|
let Some(server_config) = &self.server_config else {
|
||||||
// send post to notification webhook url
|
return;
|
||||||
|
};
|
||||||
|
let notification_config = server_config.notification().clone();
|
||||||
|
let net = self.net.clone();
|
||||||
|
async move {
|
||||||
|
match notification_config.r#type() {
|
||||||
|
NotificationType::None => { /* do nothing */ }
|
||||||
|
NotificationType::Webhook => send_webhook(msg, notification_config, net).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.in_current_span()
|
||||||
|
.into_actor(self)
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_webhook(
|
||||||
|
msg: NotifyUser,
|
||||||
|
notification_config: Notification,
|
||||||
|
net: kxio::network::Network,
|
||||||
|
) {
|
||||||
|
let Some(webhook_config) = notification_config.webhook() else {
|
||||||
|
tracing::warn!("Invalid notification configuration (config) - can't sent notification");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(webhook) = Webhook::new(webhook_config.secret().expose_secret()) else {
|
||||||
|
tracing::warn!("Invalid notification configuration (signer) - can't sent notification");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
do_send_webhook(msg, webhook, webhook_config, net).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_send_webhook(
|
||||||
|
msg: NotifyUser,
|
||||||
|
webhook: Webhook,
|
||||||
|
webhook_config: &git_next_config::server::OutboundWebhook,
|
||||||
|
net: kxio::network::Network,
|
||||||
|
) {
|
||||||
|
let message_id = format!("msg_{}", ulid::Ulid::new());
|
||||||
|
let timestamp = time::OffsetDateTime::now_utc();
|
||||||
|
let payload = msg.as_json(timestamp);
|
||||||
|
let timestamp = timestamp.unix_timestamp();
|
||||||
|
let to_sign = format!("{message_id}.{timestamp}.{payload}");
|
||||||
|
tracing::info!(?to_sign, "");
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
let signature = webhook
|
||||||
|
.sign(&message_id, timestamp, payload.to_string().as_ref())
|
||||||
|
.expect("signature");
|
||||||
|
tracing::info!(?signature, "");
|
||||||
|
let url = webhook_config.url();
|
||||||
|
use kxio::network::{NetRequest, NetUrl, RequestBody, ResponseType};
|
||||||
|
let net_url = NetUrl::new(url.to_string());
|
||||||
|
let request = NetRequest::post(net_url)
|
||||||
|
.body(RequestBody::Json(payload))
|
||||||
|
.header("webhook-id", &message_id)
|
||||||
|
.header("webhook-timestamp", ×tamp.to_string())
|
||||||
|
.header("webhook-signature", &signature)
|
||||||
|
.response_type(ResponseType::None)
|
||||||
|
.build();
|
||||||
|
net.post_json::<()>(request).await.map_or_else(
|
||||||
|
|err| {
|
||||||
|
tracing::warn!(?err, "sending webhook");
|
||||||
|
},
|
||||||
|
|_| (),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
|
||||||
server_storage,
|
server_storage,
|
||||||
} = msg.unwrap();
|
} = msg.unwrap();
|
||||||
// shutdown any existing webhook actor
|
// shutdown any existing webhook actor
|
||||||
if let Some(webhook_actor_addr) = self.webhook.take() {
|
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
|
||||||
webhook_actor_addr.do_send(ShutdownWebhook);
|
webhook_actor_addr.do_send(ShutdownWebhook);
|
||||||
}
|
}
|
||||||
self.generation.inc();
|
self.generation.inc();
|
||||||
|
@ -52,8 +52,9 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
|
||||||
.insert((forge_alias.clone(), repo_alias), addr);
|
.insert((forge_alias.clone(), repo_alias), addr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let webhook = WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
let webhook_actor_addr =
|
||||||
|
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
||||||
|
self.webhook_actor_addr.replace(webhook_actor_addr);
|
||||||
self.server_config.replace(server_config);
|
self.server_config.replace(server_config);
|
||||||
self.webhook.replace(webhook);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ impl Handler<Shutdown> for ServerActor {
|
||||||
tracing::debug!(%forge_alias, %repo_alias, "removed webhook");
|
tracing::debug!(%forge_alias, %repo_alias, "removed webhook");
|
||||||
});
|
});
|
||||||
tracing::debug!("server shutdown");
|
tracing::debug!("server shutdown");
|
||||||
if let Some(webhook) = self.webhook.take() {
|
if let Some(webhook) = self.webhook_actor_addr.take() {
|
||||||
tracing::debug!("shuting down webhook");
|
tracing::debug!("shuting down webhook");
|
||||||
webhook.do_send(ShutdownWebhook);
|
webhook.do_send(ShutdownWebhook);
|
||||||
tracing::debug!("webhook shutdown");
|
tracing::debug!("webhook shutdown");
|
||||||
|
|
|
@ -47,7 +47,7 @@ type Result<T> = core::result::Result<T, Error>;
|
||||||
pub struct ServerActor {
|
pub struct ServerActor {
|
||||||
server_config: Option<ServerConfig>,
|
server_config: Option<ServerConfig>,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
webhook: Option<Addr<WebhookActor>>,
|
webhook_actor_addr: Option<Addr<WebhookActor>>,
|
||||||
fs: FileSystem,
|
fs: FileSystem,
|
||||||
net: Network,
|
net: Network,
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
|
@ -72,7 +72,7 @@ impl ServerActor {
|
||||||
Self {
|
Self {
|
||||||
server_config: None,
|
server_config: None,
|
||||||
generation,
|
generation,
|
||||||
webhook: None,
|
webhook_actor_addr: None,
|
||||||
fs,
|
fs,
|
||||||
net,
|
net,
|
||||||
repository_factory: repo,
|
repository_factory: repo,
|
||||||
|
|
Loading…
Reference in a new issue