feat(server/webhook): implement register webhook

Closes kemitix/git-next#15
This commit is contained in:
Paul Campbell 2024-04-14 06:48:26 +01:00
parent e7060800eb
commit dd91aa4f69
7 changed files with 116 additions and 54 deletions

View file

@ -4,7 +4,6 @@ use kxio::network::{self, Network};
use tracing::{error, info, warn};
use crate::server::{
actors::repo::ValidateRepo,
config::{self, RepoConfig, RepoDetails},
forge::{self, CommitHistories},
git::Git,
@ -27,7 +26,6 @@ pub async fn validate_positions(
Ok(commit_histories) => commit_histories,
Err(err) => {
error!(?err, "Failed to get commit histories");
revalidate_positions(addr).await;
return;
}
};
@ -35,20 +33,17 @@ pub async fn validate_positions(
// Validations
let Some(main) = commit_histories.main.first().cloned() else {
warn!("No commits on main branch '{}'", config.branches().main());
revalidate_positions(addr).await;
return;
};
// verify that next is an ancestor of dev, and force update to it main if it isn't
let Some(next) = commit_histories.next.first().cloned() else {
warn!("No commits on next branch '{}", config.branches().next());
revalidate_positions(addr).await;
return;
};
let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next);
if !next_is_ancestor_of_dev {
info!("Next is not an ancestor of dev - resetting next to main");
reset_next_to_main(next, main, &repo_details, &config, &git);
revalidate_positions(addr).await;
return;
}
@ -64,12 +59,10 @@ pub async fn validate_positions(
config.branches().next()
);
reset_next_to_main(next, main, &repo_details, &config, &git);
revalidate_positions(addr).await;
return;
}
let Some(next) = next_commits.first().cloned() else {
warn!("No commits on next branch '{}'", config.branches().next());
revalidate_positions(addr).await;
return;
};
let dev_has_next = commit_histories
@ -82,7 +75,6 @@ pub async fn validate_positions(
config.branches().dev(),
config.branches().next()
);
revalidate_positions(addr).await;
return; // dev is not based on next
}
let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main);
@ -93,12 +85,10 @@ pub async fn validate_positions(
config.branches().main(),
config.branches().main(),
);
revalidate_positions(addr).await;
return;
}
let Some(dev) = commit_histories.dev.first().cloned() else {
warn!("No commits on dev branch '{}'", config.branches().dev());
revalidate_positions(addr).await;
return;
};
addr.do_send(StartMonitoring {
@ -144,12 +134,6 @@ fn reset_next_to_main(
}
}
async fn revalidate_positions(addr: Addr<RepoActor>) {
// TODO : (#43) sleep and restart while we don't have webhooks
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
addr.do_send(ValidateRepo)
}
// advance next to the next commit towards the head of the dev branch
#[tracing::instrument(fields(next), skip_all)]
pub async fn advance_next(
@ -157,18 +141,15 @@ pub async fn advance_next(
dev_commit_history: Vec<forge::Commit>,
repo_details: config::RepoDetails,
repo_config: config::RepoConfig,
addr: Addr<RepoActor>,
git: Git,
) {
let next_commit = find_next_commit_on_dev(next, dev_commit_history);
let Some(commit) = next_commit else {
warn!("No commits to advance next to");
revalidate_positions(addr).await;
return;
};
if let Some(problem) = validate_commit_message(commit.message()) {
warn!("Can't advance next to commit '{}': {}", commit, problem);
revalidate_positions(addr).await;
return;
}
info!("Advancing next to commit '{}'", commit);
@ -180,7 +161,6 @@ pub async fn advance_next(
) {
warn!(?err, "Failed")
};
revalidate_positions(addr).await;
}
#[tracing::instrument]

View file

@ -8,7 +8,7 @@ use kxio::network::Network;
use tracing::{info, warn};
use crate::server::{
config::{RepoConfig, RepoDetails},
config::{RepoConfig, RepoDetails, Webhook},
forge,
git::Git,
};
@ -17,17 +17,24 @@ use self::webhook::WebhookId;
pub struct RepoActor {
details: RepoDetails,
config: Option<RepoConfig>, // INFO: if [None] then send [StartRepo] to populate it
webhook: Webhook,
webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
webhook_auth: Option<ulid::Ulid>,
net: Network,
git: Git,
}
impl RepoActor {
pub(crate) const fn new(details: RepoDetails, net: Network, git: Git) -> Self {
pub(crate) const fn new(
details: RepoDetails,
webhook: Webhook,
net: Network,
git: Git,
) -> Self {
Self {
details,
config: None,
webhook,
webhook_id: None,
webhook_auth: None,
net,
git,
}
@ -72,7 +79,18 @@ impl Handler<LoadedConfig> for RepoActor {
fn handle(&mut self, msg: LoadedConfig, ctx: &mut Self::Context) -> Self::Result {
let config = msg.0;
info!(%self.details, %config, "Config loaded");
self.config.replace(config);
self.details.config.replace(config);
if self.webhook_id.is_none() {
info!("lets register the webhook...");
webhook::register(
self.details.clone(),
self.webhook.clone(),
ctx.address(),
self.net.clone(),
)
.into_actor(self)
.wait(ctx);
}
ctx.address().do_send(ValidateRepo);
}
}
@ -83,7 +101,7 @@ pub struct ValidateRepo;
impl Handler<ValidateRepo> for RepoActor {
type Result = ();
fn handle(&mut self, _msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
if let Some(repo_config) = self.config.clone() {
if let Some(repo_config) = self.details.config.clone() {
let repo_details = self.details.clone();
let addr = ctx.address();
let net = self.net.clone();
@ -106,7 +124,7 @@ pub struct StartMonitoring {
impl Handler<StartMonitoring> for RepoActor {
type Result = ();
fn handle(&mut self, msg: StartMonitoring, ctx: &mut Self::Context) -> Self::Result {
let Some(repo_config) = self.config.clone() else {
let Some(repo_config) = self.details.config.clone() else {
warn!("No config loaded");
return;
};
@ -116,6 +134,7 @@ impl Handler<StartMonitoring> for RepoActor {
info!(%msg.main, %msg.next, %msg.dev, next_ahead_of_main, dev_ahead_of_next, "StartMonitoring");
let repo_details = self.details.clone();
let webhook = self.webhook.clone();
let addr = ctx.address();
let net = self.net.clone();
let git = self.git.clone();
@ -130,13 +149,12 @@ impl Handler<StartMonitoring> for RepoActor {
msg.dev_commit_history,
repo_details,
repo_config,
addr,
git,
)
.into_actor(self)
.wait(ctx);
} else if self.webhook_id.is_none() {
webhook::register(repo_details, addr, net)
webhook::register(repo_details, webhook, addr, net)
.into_actor(self)
.wait(ctx);
}
@ -145,11 +163,12 @@ impl Handler<StartMonitoring> for RepoActor {
#[derive(Message)]
#[rtype(result = "()")]
pub struct WebhookRegistered(pub WebhookId);
pub struct WebhookRegistered(pub WebhookId, pub ulid::Ulid);
impl Handler<WebhookRegistered> for RepoActor {
type Result = ();
fn handle(&mut self, msg: WebhookRegistered, _ctx: &mut Self::Context) -> Self::Result {
self.webhook_id.replace(msg.0);
self.webhook_auth.replace(msg.1);
}
}
@ -160,7 +179,7 @@ impl Handler<AdvanceMainTo> for RepoActor {
type Result = ();
fn handle(&mut self, msg: AdvanceMainTo, ctx: &mut Self::Context) -> Self::Result {
let repo_details = self.details.clone();
let Some(repo_config) = self.config.clone() else {
let Some(repo_config) = self.details.config.clone() else {
warn!("No config loaded");
return;
};

View file

@ -3,7 +3,6 @@ use gix::trace::warn;
use tracing::info;
use crate::server::{
actors::repo::ValidateRepo,
config::{self, ForgeType},
forge,
};
@ -37,9 +36,6 @@ pub async fn check_next(
warn!("Checks have failed");
}
}
// TODO : (#43) sleep and restart while we don't have webhooks
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
addr.do_send(ValidateRepo);
}
#[derive(Debug)]

View file

@ -1,12 +1,15 @@
use actix::prelude::*;
use kxio::network;
use kxio::network::{self, json};
use tracing::{info, warn};
use std::{fmt::Display, ops::Deref};
use crate::server::actors::{
repo::{RepoActor, ValidateRepo},
webhook::WebhookMessage,
use crate::server::{
actors::{
repo::{RepoActor, WebhookRegistered},
webhook::WebhookMessage,
},
config::Webhook,
};
#[derive(Clone, Debug, PartialEq, Eq)]
@ -37,8 +40,10 @@ pub async fn unregister(
info!(?webhook_id, "unregister webhook");
let hostname = &repo_details.forge.hostname;
let path = repo_details.repo;
use secrecy::ExposeSecret;
let token = repo_details.forge.token.expose_secret();
let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{path}/hooks/{webhook_id}"
"https://{hostname}/api/v1/repos/{path}/hooks/{webhook_id}?token={token}"
));
let request = network::NetRequest::new(
network::RequestMethod::Delete,
@ -57,13 +62,66 @@ pub async fn unregister(
}
pub async fn register(
_repo_details: crate::server::config::RepoDetails,
repo_details: crate::server::config::RepoDetails,
webhook: Webhook,
addr: actix::prelude::Addr<super::RepoActor>,
_net: network::Network,
net: network::Network,
) {
// TODO: (#15) register webhook - on success send webhook id to RepoActor
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
addr.do_send(ValidateRepo);
let Some(repo_config) = repo_details.config else {
return;
};
info!("Registering webhook");
let hostname = &repo_details.forge.hostname;
let path = repo_details.repo;
use secrecy::ExposeSecret;
let token = repo_details.forge.token.expose_secret();
let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{path}/hooks?token={token}"
));
let webhook_url = webhook.url();
let repo_alias = repo_details.name;
let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json");
let authorisation = ulid::Ulid::new();
let body = json!({
"active": true,
"authorization_header": format!("Basic {}", authorisation),
"branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()),
"config": {
"content_type": "json",
"url": format!("{}/{}", webhook_url.as_ref(), repo_alias),
},
"events": [ "push" ],
"type": "forgejo"
});
let request = network::NetRequest::new(
network::RequestMethod::Post,
url,
headers,
network::RequestBody::Json(body),
network::ResponseType::Json,
None,
network::NetRequestLogging::None,
);
let result = net.post_json::<Hook>(request).await;
match result {
Ok(response) => {
if let Some(hook) = response.response_body() {
info!("Webhook registered");
addr.do_send(WebhookRegistered(hook.id(), authorisation));
}
}
Err(_) => warn!("Failed to register webhook"),
}
}
#[derive(Debug, serde::Deserialize)]
struct Hook {
id: i64,
}
impl Hook {
fn id(&self) -> WebhookId {
WebhookId(format!("{}", self.id))
}
}
impl Handler<WebhookMessage> for RepoActor {

View file

@ -26,10 +26,14 @@ impl ServerConfig {
.iter()
.map(|(name, forge)| (ForgeName(name.clone()), forge))
}
pub const fn webhook(&self) -> &Webhook {
&self.webhook
}
}
/// Defines the Webhook Forges should send updates to
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Webhook {
url: String,
}

View file

@ -1,6 +1,5 @@
use base64::Engine;
use kxio::network::{self, Network};
use secrecy::ExposeSecret;
use terrors::OneOf;
use tracing::{error, info};
@ -34,6 +33,7 @@ pub async fn load(
let path = &details.repo;
let filepath = ".git-next.toml";
let branch = &details.branch;
use secrecy::ExposeSecret;
let token = details.forge.token.expose_secret();
let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{path}/contents/{filepath}?ref={branch}&token={token}"
@ -128,6 +128,7 @@ pub async fn validate(
) -> Result<RepoConfig, OneOf<RepoConfigValidateErrors>> {
let hostname = &details.forge.hostname;
let path = &details.repo;
use secrecy::ExposeSecret;
let token = details.forge.token.expose_secret();
let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{path}/branches?token={token}"

View file

@ -15,7 +15,7 @@ use crate::{
filesystem::FileSystem,
server::{
actors::webhook,
config::{Forge, ForgeName, RepoAlias},
config::{Forge, ForgeName, RepoAlias, Webhook},
git::Git,
},
};
@ -46,19 +46,19 @@ pub async fn start(fs: FileSystem, net: Network, git: Git) {
return;
};
info!("Starting Server...");
let config = match config::ServerConfig::load(&fs) {
Ok(config) => config,
let server_config = match config::ServerConfig::load(&fs) {
Ok(server_config) => server_config,
Err(err) => {
error!("Failed to load config file. Error: {}", err);
return;
}
};
info!("Config loaded");
let webhook_router = webhook::WebhookRouter::new().start();
config
let webhook = server_config.webhook();
server_config
.forges()
.flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, &net, &git))
.flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, webhook, &net, &git))
.map(start_actor)
.map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient()))
.for_each(|msg| webhook_router.do_send(msg));
@ -71,6 +71,7 @@ pub async fn start(fs: FileSystem, net: Network, git: Git) {
fn create_forge_repos(
forge: &Forge,
forge_name: ForgeName,
webhook: &Webhook,
net: &Network,
git: &Git,
) -> Vec<(ForgeName, RepoAlias, RepoActor)> {
@ -80,16 +81,18 @@ fn create_forge_repos(
info!("Creating Forge");
forge
.repos()
.map(create_actor(forge_name, forge.clone(), net, git))
.map(create_actor(forge_name, forge.clone(), webhook, net, git))
.collect::<Vec<_>>()
}
fn create_actor(
forge_name: ForgeName,
forge: config::Forge,
webhook: &Webhook,
net: &Network,
git: &Git,
) -> impl Fn((RepoAlias, &Repo)) -> (ForgeName, RepoAlias, RepoActor) {
let webhook = webhook.clone();
let net = net.clone();
let git = git.clone();
move |(repo_name, repo)| {
@ -98,6 +101,7 @@ fn create_actor(
info!("Creating Repo");
let actor = actors::repo::RepoActor::new(
config::RepoDetails::new(&repo_name, repo, &forge_name, &forge),
webhook.clone(),
net.clone(),
git.clone(),
);