feat!: restructured server config into listen & shout sections
All checks were successful
Rust / build (push) Successful in 1m30s
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
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful

Groups 'http' and 'webhook' sections under 'listen'.

Renames 'notification' section as 'shout'.
This commit is contained in:
Paul Campbell 2024-07-31 07:20:42 +01:00 committed by Paul Campbell
parent 8df7600053
commit 538728c491
27 changed files with 300 additions and 235 deletions

View file

@ -21,6 +21,9 @@ nursery = { level = "warn", priority = -1 }
unwrap_used = "warn" unwrap_used = "warn"
expect_used = "warn" expect_used = "warn"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
[workspace.dependencies] [workspace.dependencies]
git-next-core = { path = "crates/core", version = "0.12" } git-next-core = { path = "crates/core", version = "0.12" }
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.12" } git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.12" }

View file

@ -79,3 +79,6 @@ nursery = { level = "warn", priority = -1 }
# pedantic = "warn" # pedantic = "warn"
unwrap_used = "warn" unwrap_used = "warn"
expect_used = "warn" expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

@ -87,7 +87,9 @@ happen to have those same names.
The server is configured by the `git-next-server.toml` file. The server is configured by the `git-next-server.toml` file.
#### http #### listen
##### http
The server needs to be able to receive webhook notifications from your forge, The server needs to be able to receive webhook notifications from your forge,
(e.g. github.com). You can do this via any method that suits your environment, (e.g. github.com). You can do this via any method that suits your environment,
@ -100,17 +102,9 @@ This is the address and port that your reverse proxy should route traffic to.
- **addr** - the IP address the server should bind to - **addr** - the IP address the server should bind to
- **port** - the IP port the server should bind to - **port** - the IP port the server should bind to
#### notification ##### url
The server should be able to notify the user when manual intervention is required. The HTTPS URL for forges to send webhooks to.
Currently this is only available via sending a Webhook message.
- **type** - one of `None` or `Webhook`
- **webhook** - the URL to POST the notification to
See [Notifications](#notifications) for more details.
#### webhook
Your forges need to know where they should route webhooks to. This should be Your forges need to know where they should route webhooks to. This should be
an address this is accessible to the forge. So, for github.com, it would need an address this is accessible to the forge. So, for github.com, it would need
@ -118,7 +112,16 @@ to be a publicly accessible HTTPS URL. For a self-hosted forge, e.g. ForgeJo,
on your own network, then it only needs to be accessible from the server your on your own network, then it only needs to be accessible from the server your
forge is running on. forge is running on.
- **url** - the HTTPS URL for forges to send webhook to #### shout
The server should be able to notify the user when manual intervention is required.
##### webhook
- **url** - the URL to POST the notification to and the
- **secret** - the sync key used to sign the webhook payload
See [Notifications](#notifications) for more details.
#### storage #### storage
@ -214,13 +217,11 @@ Currently `git-next` can only use a `gitdir` if the forge and repo is the
same one specified as the `origin` remote. Otherwise the behaviour is same one specified as the `origin` remote. Otherwise the behaviour is
untested and undefined. untested and undefined.
## Notifications ## Webhook Notifications
`git-next` can send a number of notification to the user when intervention is required. When sending a Webhook Notification to a user they are sent using the
Currently, only WebHooks are supported. Standard Webhooks format. That means all POST messages have the
following headers:
Webhooks are sent using the Standard Webhooks format. That means all POST messages have
the following headers:
- `Webhook-Id` - `Webhook-Id`
- `Webhook-Signature` - `Webhook-Signature`

View file

@ -18,14 +18,16 @@ impl Handler<RegisterWebhook> for RepoActor {
if self.webhook_id.is_none() { if self.webhook_id.is_none() {
let forge_alias = self.repo_details.forge.forge_alias().clone(); let forge_alias = self.repo_details.forge.forge_alias().clone();
let repo_alias = self.repo_details.repo_alias.clone(); let repo_alias = self.repo_details.repo_alias.clone();
let webhook_url = self.webhook.url(&forge_alias, &repo_alias); let repo_listen_url = self
.listen_url
.repo_url(forge_alias.clone(), repo_alias.clone());
let forge = self.forge.duplicate(); let forge = self.forge.duplicate();
let addr = ctx.address(); let addr = ctx.address();
let notify_user_recipient = self.notify_user_recipient.clone(); let notify_user_recipient = self.notify_user_recipient.clone();
let log = self.log.clone(); let log = self.log.clone();
debug!("registering webhook"); debug!("registering webhook");
async move { async move {
match forge.register_webhook(&webhook_url).await { match forge.register_webhook(&repo_listen_url).await {
Ok(registered_webhook) => { Ok(registered_webhook) => {
debug!(?registered_webhook, ""); debug!(?registered_webhook, "");
do_send( do_send(

View file

@ -13,7 +13,8 @@ use git_next_core::{
repository::{factory::RepositoryFactory, open::OpenRepositoryLike}, repository::{factory::RepositoryFactory, open::OpenRepositoryLike},
UserNotification, UserNotification,
}, },
server, WebhookAuth, WebhookId, server::ListenUrl,
WebhookAuth, WebhookId,
}; };
mod branch; mod branch;
@ -45,7 +46,7 @@ pub struct RepoActor {
generation: git::Generation, generation: git::Generation,
message_token: messages::MessageToken, message_token: messages::MessageToken,
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
webhook: server::InboundWebhook, listen_url: ListenUrl,
webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured
last_main_commit: Option<git::Commit>, last_main_commit: Option<git::Commit>,
@ -63,7 +64,7 @@ impl RepoActor {
pub fn new( pub fn new(
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
forge: Box<dyn git::ForgeLike>, forge: Box<dyn git::ForgeLike>,
webhook: server::InboundWebhook, listen_url: ListenUrl,
generation: git::Generation, generation: git::Generation,
net: Network, net: Network,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
@ -75,7 +76,7 @@ impl RepoActor {
generation, generation,
message_token, message_token,
repo_details, repo_details,
webhook, listen_url,
webhook_id: None, webhook_id: None,
webhook_auth: None, webhook_auth: None,
last_main_commit: None, last_main_commit: None,

View file

@ -1,3 +1,5 @@
use git_next_core::server::ListenUrl;
// //
use super::*; use super::*;
@ -50,8 +52,8 @@ pub fn a_network() -> kxio::network::MockNetwork {
kxio::network::MockNetwork::new() kxio::network::MockNetwork::new()
} }
pub fn a_webhook_url(forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> WebhookUrl { pub fn a_listen_url() -> ListenUrl {
InboundWebhook::new(a_name()).url(forge_alias, repo_alias) ListenUrl::new(a_name())
} }
pub fn a_name() -> String { pub fn a_name() -> String {
@ -168,10 +170,6 @@ pub fn a_message_token() -> MessageToken {
MessageToken::default() MessageToken::default()
} }
pub fn a_webhook(url: &WebhookUrl) -> InboundWebhook {
InboundWebhook::new(url.clone().into())
}
pub fn a_forge() -> Box<MockForgeLike> { pub fn a_forge() -> Box<MockForgeLike> {
Box::new(MockForgeLike::new()) Box::new(MockForgeLike::new())
} }
@ -182,10 +180,7 @@ pub fn a_repo_actor(
forge: Box<dyn ForgeLike>, forge: Box<dyn ForgeLike>,
net: kxio::network::Network, net: kxio::network::Network,
) -> (RepoActor, RepoActorLog) { ) -> (RepoActor, RepoActorLog) {
let forge_alias = repo_details.forge.forge_alias(); let listen_url = given::a_listen_url();
let repo_alias = &repo_details.repo_alias;
let webhook_url = given::a_webhook_url(forge_alias, repo_alias);
let webhook = given::a_webhook(&webhook_url);
let generation = Generation::default(); let generation = Generation::default();
let log = RepoActorLog::default(); let log = RepoActorLog::default();
let actors_log = log.clone(); let actors_log = log.clone();
@ -193,7 +188,7 @@ pub fn a_repo_actor(
RepoActor::new( RepoActor::new(
repo_details, repo_details,
forge, forge,
webhook, listen_url,
generation, generation,
net, net,
repository_factory, repository_factory,

View file

@ -21,7 +21,6 @@ use git_next_core::{
Commit, ForgeLike, Generation, MockForgeLike, RepoDetails, Commit, ForgeLike, Generation, MockForgeLike, RepoDetails,
}, },
message, message,
server::{InboundWebhook, WebhookUrl},
webhook::{forge_notification::Body, Push}, webhook::{forge_notification::Body, Push},
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname,
RegisteredWebhook, RemoteUrl, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource, RegisteredWebhook, RemoteUrl, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource,

View file

@ -16,10 +16,10 @@ impl Handler<NotifyUser> for ServerActor {
let Some(server_config) = &self.server_config else { let Some(server_config) = &self.server_config else {
return; return;
}; };
let notification_config = server_config.notification().clone(); let shout_config = server_config.shout().clone();
let net = self.net.clone(); let net = self.net.clone();
async move { async move {
if let Some(webhook_config) = notification_config.webhook() { if let Some(webhook_config) = shout_config.webhook() {
send_webhook(msg, webhook_config, net).await; send_webhook(msg, webhook_config, net).await;
} }
} }

View file

@ -11,7 +11,7 @@ impl Handler<ReceiveServerConfig> for ServerActor {
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn handle(&mut self, msg: ReceiveServerConfig, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveServerConfig, ctx: &mut Self::Context) -> Self::Result {
tracing::info!("recieved server config"); tracing::info!("recieved server config");
let Ok(socket_addr) = msg.http() else { let Ok(socket_addr) = msg.listen_socket_addr() else {
return self.abort(ctx, "Unable to parse http.addr"); return self.abort(ctx, "Unable to parse http.addr");
}; };
@ -19,7 +19,7 @@ impl Handler<ReceiveServerConfig> for ServerActor {
return self.abort(ctx, "Server storage not available"); return self.abort(ctx, "Server storage not available");
}; };
if msg.inbound_webhook().base_url().ends_with('/') { if msg.listen().url().ends_with('/') {
return self.abort(ctx, "webhook.url must not end with a '/'"); return self.abort(ctx, "webhook.url must not end with a '/'");
} }

View file

@ -32,7 +32,7 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
// Webhook Server // Webhook Server
info!("Starting Webhook Server..."); info!("Starting Webhook Server...");
let webhook_router = WebhookRouter::default().start(); let webhook_router = WebhookRouter::default().start();
let inbound_webhook = server_config.inbound_webhook(); let listen_url = server_config.listen().url();
// Forge Actors // Forge Actors
for (forge_alias, forge_config) in server_config.forges() { for (forge_alias, forge_config) in server_config.forges() {
let repo_actors = self let repo_actors = self
@ -40,7 +40,7 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
forge_config, forge_config,
forge_alias.clone(), forge_alias.clone(),
&server_storage, &server_storage,
inbound_webhook, listen_url,
ctx.address().recipient(), ctx.address().recipient(),
) )
.into_iter() .into_iter()

View file

@ -20,7 +20,7 @@ use crate::{
use git_next_core::{ use git_next_core::{
git::{repository::factory::RepositoryFactory, Generation, RepoDetails}, git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
server::{self, InboundWebhook, ServerConfig, ServerStorage}, server::{self, ListenUrl, ServerConfig, ServerStorage},
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
}; };
@ -113,7 +113,7 @@ impl ServerActor {
forge_config: &ForgeConfig, forge_config: &ForgeConfig,
forge_name: ForgeAlias, forge_name: ForgeAlias,
server_storage: &ServerStorage, server_storage: &ServerStorage,
webhook: &InboundWebhook, listen_url: &ListenUrl,
notify_user_recipient: Recipient<NotifyUser>, notify_user_recipient: Recipient<NotifyUser>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
let span = let span =
@ -122,7 +122,8 @@ impl ServerActor {
let _guard = span.enter(); let _guard = span.enter();
tracing::info!("Creating Forge"); tracing::info!("Creating Forge");
let mut repos = vec![]; let mut repos = vec![];
let creator = self.create_actor(forge_name, forge_config.clone(), server_storage, webhook); let creator =
self.create_actor(forge_name, forge_config.clone(), server_storage, listen_url);
for (repo_alias, server_repo_config) in forge_config.repos() { for (repo_alias, server_repo_config) in forge_config.repos() {
let forge_repo = creator(( let forge_repo = creator((
repo_alias, repo_alias,
@ -143,17 +144,16 @@ impl ServerActor {
forge_name: ForgeAlias, forge_name: ForgeAlias,
forge_config: ForgeConfig, forge_config: ForgeConfig,
server_storage: &ServerStorage, server_storage: &ServerStorage,
webhook: &InboundWebhook, listen_url: &ListenUrl,
) -> impl Fn( ) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>), (RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
) -> (ForgeAlias, RepoAlias, RepoActor) { ) -> (ForgeAlias, RepoAlias, RepoActor) {
let server_storage = server_storage.clone(); let server_storage = server_storage.clone();
let webhook = webhook.clone(); let listen_url = listen_url.clone();
let net = self.net.clone(); let net = self.net.clone();
let repository_factory = self.repository_factory.duplicate(); let repository_factory = self.repository_factory.duplicate();
let generation = self.generation; let generation = self.generation;
let sleep_duration = self.sleep_duration; let sleep_duration = self.sleep_duration;
// let notify_user_recipient = server_addr.recipient();
move |(repo_alias, server_repo_config, notify_user_recipient)| { move |(repo_alias, server_repo_config, notify_user_recipient)| {
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
let _guard = span.enter(); let _guard = span.enter();
@ -185,7 +185,7 @@ impl ServerActor {
let actor = RepoActor::new( let actor = RepoActor::new(
repo_details, repo_details,
forge, forge,
webhook.clone(), listen_url.clone(),
generation, generation,
net.clone(), net.clone(),
repository_factory.duplicate(), repository_factory.duplicate(),
@ -230,13 +230,13 @@ impl ServerActor {
} }
/// Attempts to gracefully shutdown the server before stopping the system. /// Attempts to gracefully shutdown the server before stopping the system.
fn abort(&mut self, ctx: &mut <Self as actix::Actor>::Context, message: impl Into<String>) { fn abort(&self, ctx: &mut <Self as actix::Actor>::Context, message: impl Into<String>) {
tracing::error!("Aborting: {}", message.into()); tracing::error!("Aborting: {}", message.into());
self.do_send(crate::server::actor::messages::Shutdown, ctx); self.do_send(crate::server::actor::messages::Shutdown, ctx);
System::current().stop_with_code(1); System::current().stop_with_code(1);
} }
fn do_send<M>(&mut self, msg: M, _ctx: &mut <Self as actix::Actor>::Context) fn do_send<M>(&self, msg: M, _ctx: &mut <Self as actix::Actor>::Context)
where where
M: actix::Message + Send + 'static + std::fmt::Debug, M: actix::Message + Send + 'static + std::fmt::Debug,
Self: actix::Handler<M>, Self: actix::Handler<M>,

View file

@ -4,7 +4,7 @@ use actix::prelude::*;
use crate::server::actor::{tests::given, ReceiveServerConfig, ServerActor}; use crate::server::actor::{tests::given, ReceiveServerConfig, ServerActor};
use git_next_core::{ use git_next_core::{
git, git,
server::{Http, InboundWebhook, Notification, ServerConfig, ServerStorage}, server::{Http, Listen, ListenUrl, ServerConfig, ServerStorage, Shout},
}; };
use std::{ use std::{
@ -25,9 +25,11 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
let server = ServerActor::new(fs.clone(), net.into(), repo, duration); let server = ServerActor::new(fs.clone(), net.into(), repo, duration);
// collaborators // collaborators
let http = Http::new("0.0.0.0".to_string(), 80); let listen = Listen::new(
let webhook = InboundWebhook::new("http://localhost/".to_string()); // With trailing slash Http::new("0.0.0.0".to_string(), 80),
let notifications = Notification::none(); ListenUrl::new("http://localhost/".to_string()), // with trailing slash
);
let shout = Shout::default();
let server_storage = ServerStorage::new((fs.base()).to_path_buf()); let server_storage = ServerStorage::new((fs.base()).to_path_buf());
let repos = BTreeMap::default(); let repos = BTreeMap::default();
@ -39,9 +41,8 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
server server
.start() .start()
.do_send(ReceiveServerConfig::new(ServerConfig::new( .do_send(ReceiveServerConfig::new(ServerConfig::new(
http, listen,
webhook, shout,
notifications,
server_storage, server_storage,
repos, repos,
))); )));

View file

@ -1,11 +1,12 @@
[http] # where to listen for incoming updates from forges [listen]
addr = "0.0.0.0" # The address and port to listen to for incoming webhooks from forges.
port = 8080 http = { addr = "0.0.0.0", port = 8080 }
[webhook] # where forge should send updates to - should be route to 'http.addr:http.port' above (e.g. using a reverse proxy) # The URL where forge should send updates to.
# This should be route to 'http.addr:http.port' above (e.g. using a reverse proxy)
url = "https://localhost:8080" # don't include any query path or a trailing slash url = "https://localhost:8080" # don't include any query path or a trailing slash
[notification] # where updates from git-next should be sent to alert the user [shout] # where updates from git-next should be sent to alert the user
# webhook = { url = "https//localhost:9090", secret = "secret-password" } # webhook = { url = "https//localhost:9090", secret = "secret-password" }
[storage] # where local copies of repositories will be cloned (bare) into [storage] # where local copies of repositories will be cloned (bare) into
@ -22,3 +23,9 @@ path = "./data"
# [forge.default.repos] # the repos at the forge to manage # [forge.default.repos] # the repos at the forge to manage
# hello = { repo = "bob/hello", branch = "main", gitdir = "/opt/git/projects/bob/hello.git" } # hello = { repo = "bob/hello", branch = "main", gitdir = "/opt/git/projects/bob/hello.git" }
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }
# hello = { repo = "bob/hello", branch = "main", gitdir = "/opt/git/projects/bob/hello.git" }
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }

View file

@ -61,3 +61,6 @@ nursery = { level = "warn", priority = -1 }
# pedantic = "warn" # pedantic = "warn"
unwrap_used = "warn" unwrap_used = "warn"
expect_used = "warn" expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

@ -3,12 +3,15 @@
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
net::SocketAddr, net::SocketAddr,
ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use derive_more::Display;
use kxio::fs::FileSystem; use kxio::fs::FileSystem;
use secrecy::Secret; use secrecy::Secret;
use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
use crate::{ use crate::{
@ -44,9 +47,8 @@ type Result<T> = core::result::Result<T, Error>;
derive_more::Constructor, derive_more::Constructor,
)] )]
pub struct ServerConfig { pub struct ServerConfig {
http: Http, listen: Listen,
webhook: InboundWebhook, shout: Shout,
notification: Notification,
storage: ServerStorage, storage: ServerStorage,
pub forge: BTreeMap<String, ForgeConfig>, pub forge: BTreeMap<String, ForgeConfig>,
} }
@ -69,16 +71,75 @@ impl ServerConfig {
&self.storage &self.storage
} }
pub const fn notification(&self) -> &Notification { pub const fn shout(&self) -> &Shout {
&self.notification &self.shout
} }
pub const fn inbound_webhook(&self) -> &InboundWebhook { pub const fn listen(&self) -> &Listen {
&self.webhook &self.listen
} }
pub fn http(&self) -> Result<SocketAddr> { pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
self.http.socket_addr() self.listen.http.socket_addr()
}
}
/// Defines how the server receives webhook notifications from forges.
#[derive(
Clone,
Debug,
derive_more::From,
PartialEq,
Eq,
PartialOrd,
Ord,
derive_more::AsRef,
serde::Deserialize,
derive_more::Constructor,
)]
pub struct Listen {
http: Http,
url: ListenUrl,
}
impl Listen {
/// Returns the URL a Repo will listen to for updates from the Forge
pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl {
self.url.repo_url(forge_alias, repo_alias)
}
pub const fn url(&self) -> &ListenUrl {
&self.url
}
}
newtype!(
ListenUrl:
String, Serialize, Deserialize, PartialOrd, Ord, Display:
"The base url for receiving all webhooks from all forges"
);
impl ListenUrl {
pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl {
RepoListenUrl::new((self.clone(), forge_alias, repo_alias))
}
}
newtype!(ForgeWebhookUrl: String: "Raw URL from a forge Webhook");
newtype!(RepoListenUrl: (ListenUrl, ForgeAlias, RepoAlias): "URL to listen for webhook from forges");
impl Display for RepoListenUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.deref().0,
self.deref().1,
self.deref().2
)
}
}
impl From<RepoListenUrl> for ForgeWebhookUrl {
fn from(value: RepoListenUrl) -> Self {
Self::new(value.to_string())
} }
} }
@ -108,35 +169,6 @@ impl Http {
} }
} }
/// Defines the Webhook Forges should send updates to
/// Must be an address that is accessible from the remote forge
#[derive(
Clone,
Debug,
derive_more::From,
PartialEq,
Eq,
PartialOrd,
Ord,
derive_more::AsRef,
serde::Deserialize,
derive_more::Constructor,
)]
pub struct InboundWebhook {
url: String,
}
impl InboundWebhook {
pub fn url(&self, forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> WebhookUrl {
let base_url = &self.url;
WebhookUrl(format!("{base_url}/{forge_alias}/{repo_alias}"))
}
pub fn base_url(&self) -> &str {
&self.url
}
}
newtype!(WebhookUrl: String, serde::Serialize: "The URL for the webhook where forges should send their updates");
/// The directory to store server data, such as cloned repos /// The directory to store server data, such as cloned repos
#[derive( #[derive(
Clone, Clone,
@ -164,6 +196,7 @@ impl ServerStorage {
#[derive( #[derive(
Clone, Clone,
Debug, Debug,
Default,
derive_more::From, derive_more::From,
PartialEq, PartialEq,
Eq, Eq,
@ -172,13 +205,10 @@ impl ServerStorage {
derive_more::AsRef, derive_more::AsRef,
serde::Deserialize, serde::Deserialize,
)] )]
pub struct Notification { pub struct Shout {
webhook: Option<OutboundWebhook>, webhook: Option<OutboundWebhook>,
} }
impl Notification { impl Shout {
pub const fn none() -> Self {
Self { webhook: None }
}
pub const fn new_webhook(webhook: OutboundWebhook) -> Self { pub const fn new_webhook(webhook: OutboundWebhook) -> Self {
Self { Self {
webhook: Some(webhook), webhook: Some(webhook),
@ -197,11 +227,6 @@ impl Notification {
self.webhook.clone().map(|x| x.secret).map(Secret::new) self.webhook.clone().map(|x| x.secret).map(Secret::new)
} }
} }
impl Default for Notification {
fn default() -> Self {
Self::none()
}
}
#[derive( #[derive(
Clone, Clone,

View file

@ -7,7 +7,6 @@ use std::collections::BTreeMap;
use std::path::PathBuf; use std::path::PathBuf;
use crate::server::Http; use crate::server::Http;
use crate::server::InboundWebhook;
use crate::server::ServerConfig; use crate::server::ServerConfig;
use crate::server::ServerStorage; use crate::server::ServerStorage;
use crate::webhook::push::Branch; use crate::webhook::push::Branch;
@ -465,14 +464,14 @@ mod server {
server_config: &ServerConfig, server_config: &ServerConfig,
fs: &kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
) -> TestResult { ) -> TestResult {
let http = &server_config.http()?; let http = &server_config.listen_socket_addr()?;
let http_addr = http.ip(); let http_addr = http.ip();
let http_port = server_config.http()?.port(); let http_port = server_config.listen_socket_addr()?.port();
let webhook_url = server_config.inbound_webhook().base_url(); let listen_url = server_config.listen().url();
let storage_path = server_config.storage().path(); let storage_path = server_config.storage().path();
let notification = &server_config.notification(); let shout = server_config.shout();
let notification_webhook_url = notification.webhook_url().unwrap_or_default(); let shout_webhook_url = shout.webhook_url().unwrap_or_default();
let notification_webhook_secret = notification let shout_webhook_secret = shout
.webhook_secret() .webhook_secret()
.map(|secret| secret.expose_secret().clone()) .map(|secret| secret.expose_secret().clone())
.unwrap_or_default(); .unwrap_or_default();
@ -515,20 +514,16 @@ mod server {
let repos = repos.join("\n"); let repos = repos.join("\n");
let file_contents = &format!( let file_contents = &format!(
r#" r#"
[http] [listen]
addr = "{http_addr}" http = {{ addr = "{http_addr}", port = {http_port} }}
port = {http_port} url = "{listen_url}"
[webhook] [shout]
url = "{webhook_url}" webhook = {{ url = "{shout_webhook_url}", secret = "{shout_webhook_secret}" }}
[storage] [storage]
path = {storage_path:?} path = {storage_path:?}
[notification]
type = "Webhook"
webhook = {{ url = "{notification_webhook_url}", secret = "{notification_webhook_secret}" }}
[forge.{forge_alias}] [forge.{forge_alias}]
forge_type = "{forge_type}" forge_type = "{forge_type}"
hostname = "{forge_hostname}" hostname = "{forge_hostname}"
@ -697,7 +692,7 @@ mod push {
} }
mod given { mod given {
use crate::server::{Notification, OutboundWebhook}; use crate::server::{Listen, ListenUrl, OutboundWebhook, Shout};
use super::*; use super::*;
use rand::Rng as _; use rand::Rng as _;
@ -720,8 +715,7 @@ mod given {
} }
pub fn a_server_config() -> ServerConfig { pub fn a_server_config() -> ServerConfig {
ServerConfig::new( ServerConfig::new(
an_http(), a_listen(),
an_inbound_webhook(),
a_notification_config(), a_notification_config(),
a_server_storage(), a_server_storage(),
some_forge_configs(), some_forge_configs(),
@ -743,8 +737,11 @@ mod given {
pub fn a_port() -> u16 { pub fn a_port() -> u16 {
rand::thread_rng().gen() rand::thread_rng().gen()
} }
pub fn an_inbound_webhook() -> InboundWebhook { pub fn a_listen() -> Listen {
InboundWebhook::new(a_name()) Listen::new(an_http(), a_listen_url())
}
pub fn a_listen_url() -> ListenUrl {
ListenUrl::new(a_name())
} }
pub fn an_outbound_webhook() -> OutboundWebhook { pub fn an_outbound_webhook() -> OutboundWebhook {
OutboundWebhook::new(a_name(), a_name()) OutboundWebhook::new(a_name(), a_name())
@ -752,8 +749,8 @@ mod given {
pub fn a_server_storage() -> ServerStorage { pub fn a_server_storage() -> ServerStorage {
ServerStorage::new(a_name().into()) ServerStorage::new(a_name().into())
} }
pub fn a_notification_config() -> Notification { pub fn a_notification_config() -> Shout {
Notification::new_webhook(an_outbound_webhook()) Shout::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()

View file

@ -1,6 +1,7 @@
// //
use crate::{ use crate::{
git, server::WebhookUrl, webhook, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId, git, server::RepoListenUrl, webhook, ForgeNotification, RegisteredWebhook, WebhookAuth,
WebhookId,
}; };
#[mockall::automock] #[mockall::automock]
@ -29,7 +30,10 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
async fn commit_status(&self, commit: &git::Commit) -> git::forge::commit::Status; async fn commit_status(&self, commit: &git::Commit) -> git::forge::commit::Status;
// Lists all the webhooks // Lists all the webhooks
async fn list_webhooks(&self, url: &WebhookUrl) -> git::forge::webhook::Result<Vec<WebhookId>>; async fn list_webhooks(
&self,
repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<Vec<WebhookId>>;
// Unregisters a webhook // Unregisters a webhook
async fn unregister_webhook(&self, webhook: &WebhookId) -> git::forge::webhook::Result<()>; async fn unregister_webhook(&self, webhook: &WebhookId) -> git::forge::webhook::Result<()>;
@ -37,6 +41,6 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
// Registers a webhook // Registers a webhook
async fn register_webhook( async fn register_webhook(
&self, &self,
webhook_url: &WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<RegisteredWebhook>; ) -> git::forge::webhook::Result<RegisteredWebhook>;
} }

View file

@ -49,3 +49,6 @@ nursery = { level = "warn", priority = -1 }
# pedantic = "warn" # pedantic = "warn"
unwrap_used = "warn" unwrap_used = "warn"
expect_used = "warn" expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

@ -5,8 +5,10 @@ mod tests;
mod webhook; mod webhook;
use git_next_core::{ use git_next_core::{
self as core, git, git::forge::commit::Status, server, ForgeNotification, RegisteredWebhook, self as core,
WebhookAuth, WebhookId, git::{self, forge::commit::Status},
server::RepoListenUrl,
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
}; };
use kxio::network::{self, Network}; use kxio::network::{self, Network};
@ -93,9 +95,9 @@ impl git::ForgeLike for ForgeJo {
async fn list_webhooks( async fn list_webhooks(
&self, &self,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<Vec<WebhookId>> { ) -> git::forge::webhook::Result<Vec<WebhookId>> {
webhook::list(&self.repo_details, webhook_url, &self.net).await webhook::list(&self.repo_details, repo_listen_url, &self.net).await
} }
async fn unregister_webhook(&self, webhook_id: &WebhookId) -> git::forge::webhook::Result<()> { async fn unregister_webhook(&self, webhook_id: &WebhookId) -> git::forge::webhook::Result<()> {
@ -104,9 +106,9 @@ impl git::ForgeLike for ForgeJo {
async fn register_webhook( async fn register_webhook(
&self, &self,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<RegisteredWebhook> { ) -> git::forge::webhook::Result<RegisteredWebhook> {
webhook::register(&self.repo_details, webhook_url, &self.net).await webhook::register(&self.repo_details, repo_listen_url, &self.net).await
} }
} }

View file

@ -2,7 +2,7 @@
use crate::ForgeJo; use crate::ForgeJo;
use git_next_core::{ use git_next_core::{
git::{self, forge::commit::Status, ForgeLike as _}, git::{self, forge::commit::Status, ForgeLike as _},
server::{InboundWebhook, WebhookUrl}, server::ListenUrl,
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias, BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias,
RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId, RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId,
}; };
@ -198,7 +198,7 @@ mod forgejo {
async fn should_return_a_list_of_matching_webhooks() { async fn should_return_a_list_of_matching_webhooks() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let webhook_url = given::any_webhook_url(); let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
@ -217,9 +217,9 @@ mod forgejo {
with::get_webhooks_by_page( with::get_webhooks_by_page(
1, 1,
&[ &[
with::ReturnedWebhook::new(hook_id_1, &webhook_url), with::ReturnedWebhook::new(hook_id_1, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_2, &webhook_url), with::ReturnedWebhook::new(hook_id_2, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()), with::ReturnedWebhook::new(hook_id_3, &given::a_repo_listen_url(&repo_details)),
], ],
&mut args, &mut args,
); );
@ -228,7 +228,7 @@ mod forgejo {
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(result) = forge.list_webhooks(&webhook_url).await); let_assert!(Ok(result) = forge.list_webhooks(&repo_listen_url).await);
assert_eq!( assert_eq!(
result, result,
vec![ vec![
@ -242,7 +242,7 @@ mod forgejo {
async fn should_return_any_network_error() { async fn should_return_any_network_error() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let webhook_url = given::a_webhook_url(&given::a_forge_alias(), &given::a_repo_alias()); let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
@ -256,7 +256,7 @@ mod forgejo {
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(_) = forge.list_webhooks(&webhook_url).await); let_assert!(Err(_) = forge.list_webhooks(&repo_listen_url).await);
} }
} }
@ -326,8 +326,7 @@ mod forgejo {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
@ -352,7 +351,7 @@ mod forgejo {
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(registered_webhook) = forge.register_webhook(&webhook_url).await); let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!( assert_eq!(
registered_webhook.id(), registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}")) &WebhookId::new(format!("{webhook_id}"))
@ -368,11 +367,10 @@ mod forgejo {
repo_details.repo_config.is_none(), repo_details.repo_config.is_none(),
"repo_details needs to NOT have repo_config for this test" "repo_details needs to NOT have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let net = given::a_network(); let net = given::a_network();
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::NoRepoConfig), matches!(err, git::forge::webhook::Error::NoRepoConfig),
"{err:?}" "{err:?}"
@ -387,8 +385,7 @@ mod forgejo {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
@ -400,11 +397,11 @@ mod forgejo {
repo_path, repo_path,
token, token,
}; };
let hook1 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &webhook_url); let hook1 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &repo_listen_url);
let hook2 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &webhook_url); let hook2 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &repo_listen_url);
let hook3 = with::ReturnedWebhook::new( let hook3 = with::ReturnedWebhook::new(
given::a_forgejo_webhook_id(), given::a_forgejo_webhook_id(),
&given::any_webhook_url(), &given::a_repo_listen_url(&repo_details),
); );
let hooks = [hook1, hook2, hook3]; let hooks = [hook1, hook2, hook3];
@ -424,7 +421,7 @@ mod forgejo {
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(registered_webhook) = forge.register_webhook(&webhook_url).await); let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!( assert_eq!(
registered_webhook.id(), registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}")) &WebhookId::new(format!("{webhook_id}"))
@ -439,8 +436,7 @@ mod forgejo {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
@ -464,7 +460,7 @@ mod forgejo {
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}" "{err:?}"
@ -479,8 +475,7 @@ mod forgejo {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
@ -503,7 +498,7 @@ mod forgejo {
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}" "{err:?}"
@ -511,6 +506,8 @@ mod forgejo {
} }
} }
mod with { mod with {
use git_next_core::server::RepoListenUrl;
use super::*; use super::*;
pub fn get_webhooks_by_page( pub fn get_webhooks_by_page(
@ -556,20 +553,25 @@ mod forgejo {
pub config: Config, pub config: Config,
} }
impl ReturnedWebhook { impl ReturnedWebhook {
pub fn new(id: i64, url: &WebhookUrl) -> Self { pub fn new(id: i64, url: &RepoListenUrl) -> Self {
Self { Self {
id, id,
config: Config { url: url.clone() }, config: Config {
url: url.to_string(),
},
} }
} }
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Config { pub struct Config {
pub url: WebhookUrl, pub url: String,
} }
} }
mod given { mod given {
use git::RepoDetails;
use git_next_core::server::RepoListenUrl;
use super::*; use super::*;
pub fn a_commit_state( pub fn a_commit_state(
@ -674,14 +676,6 @@ mod forgejo {
kxio::network::MockNetwork::new() kxio::network::MockNetwork::new()
} }
pub fn a_webhook_url(forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> WebhookUrl {
InboundWebhook::new(a_name()).url(forge_alias, repo_alias)
}
pub fn any_webhook_url() -> WebhookUrl {
a_webhook_url(&a_forge_alias(), &a_repo_alias())
}
pub fn a_name() -> String { pub fn a_name() -> String {
use rand::Rng; use rand::Rng;
use std::iter; use std::iter;
@ -765,5 +759,16 @@ mod forgejo {
gitdir, gitdir,
) )
} }
pub fn a_repo_listen_url(repo_details: &RepoDetails) -> RepoListenUrl {
let listen_url = a_listen_url();
let forge_alias = repo_details.forge.forge_alias().clone();
let repo_alias = repo_details.repo_alias.clone();
RepoListenUrl::new((listen_url, forge_alias, repo_alias))
}
fn a_listen_url() -> ListenUrl {
ListenUrl::new(a_name())
}
} }
} }

View file

@ -1,5 +1,5 @@
// //
use git_next_core::{git, server::WebhookUrl, WebhookId}; use git_next_core::{git, server::RepoListenUrl, WebhookId};
use kxio::network; use kxio::network;
@ -7,7 +7,7 @@ use crate::webhook::Hook;
pub async fn list( pub async fn list(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
webhook_url: &WebhookUrl, repo_listen_url: &RepoListenUrl,
net: &network::Network, net: &network::Network,
) -> git::forge::webhook::Result<Vec<WebhookId>> { ) -> git::forge::webhook::Result<Vec<WebhookId>> {
let mut ids: Vec<WebhookId> = vec![]; let mut ids: Vec<WebhookId> = vec![];
@ -38,7 +38,7 @@ pub async fn list(
} }
for hook in list { for hook in list {
if let Some(existing_url) = hook.config.get("url") { if let Some(existing_url) = hook.config.get("url") {
if existing_url.starts_with(webhook_url.as_ref()) { if existing_url.starts_with(&repo_listen_url.to_string()) {
ids.push(hook.id()); ids.push(hook.id());
} }
} }

View file

@ -1,5 +1,6 @@
use git_next_core::server::RepoListenUrl;
// //
use git_next_core::{git, server, RegisteredWebhook, WebhookAuth, WebhookId}; use git_next_core::{git, RegisteredWebhook, WebhookAuth, WebhookId};
use kxio::network; use kxio::network;
use tracing::{info, warn}; use tracing::{info, warn};
@ -10,7 +11,7 @@ use crate::webhook::Hook;
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn register( pub async fn register(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
net: &network::Network, net: &network::Network,
) -> git::forge::webhook::Result<RegisteredWebhook> { ) -> git::forge::webhook::Result<RegisteredWebhook> {
let Some(repo_config) = repo_details.repo_config.clone() else { let Some(repo_config) = repo_details.repo_config.clone() else {
@ -18,7 +19,7 @@ pub async fn register(
}; };
// remove any lingering webhooks for the same URL // remove any lingering webhooks for the same URL
let existing_webhook_ids = webhook::list(repo_details, webhook_url, net).await?; let existing_webhook_ids = webhook::list(repo_details, repo_listen_url, net).await?;
for webhook_id in existing_webhook_ids { for webhook_id in existing_webhook_ids {
webhook::unregister(&webhook_id, repo_details, net).await?; webhook::unregister(&webhook_id, repo_details, net).await?;
} }
@ -30,7 +31,6 @@ pub async fn register(
let url = network::NetUrl::new(format!( let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}" "https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
)); ));
let repo_alias = &repo_details.repo_alias;
let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json"); let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json");
let authorisation = WebhookAuth::generate(); let authorisation = WebhookAuth::generate();
let body = network::json!({ let body = network::json!({
@ -39,7 +39,7 @@ pub async fn register(
"branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()), "branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()),
"config": { "config": {
"content_type": "json", "content_type": "json",
"url": format!("{}/{}", webhook_url.as_ref(), repo_alias), "url": repo_listen_url.to_string(),
}, },
"events": [ "push" ], "events": [ "push" ],
"type": "forgejo" "type": "forgejo"

View file

@ -57,3 +57,6 @@ nursery = { level = "warn", priority = -1 }
# pedantic = "warn" # pedantic = "warn"
unwrap_used = "warn" unwrap_used = "warn"
expect_used = "warn" expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

@ -9,7 +9,8 @@ use crate as github;
use git_next_core::{ use git_next_core::{
self as core, self as core,
git::{self, forge::commit::Status}, git::{self, forge::commit::Status},
server, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId, server::{self, RepoListenUrl},
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
}; };
use derive_more::Constructor; use derive_more::Constructor;
@ -57,9 +58,9 @@ impl git::ForgeLike for Github {
async fn list_webhooks( async fn list_webhooks(
&self, &self,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<Vec<WebhookId>> { ) -> git::forge::webhook::Result<Vec<WebhookId>> {
github::webhook::list(self, webhook_url).await github::webhook::list(self, repo_listen_url).await
} }
async fn unregister_webhook(&self, webhook_id: &WebhookId) -> git::forge::webhook::Result<()> { async fn unregister_webhook(&self, webhook_id: &WebhookId) -> git::forge::webhook::Result<()> {
@ -69,9 +70,9 @@ impl git::ForgeLike for Github {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook
async fn register_webhook( async fn register_webhook(
&self, &self,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<RegisteredWebhook> { ) -> git::forge::webhook::Result<RegisteredWebhook> {
github::webhook::register(self, webhook_url).await github::webhook::register(self, repo_listen_url).await
} }
} }
@ -103,8 +104,8 @@ impl GithubHook {
pub fn id(&self) -> WebhookId { pub fn id(&self) -> WebhookId {
WebhookId::new(format!("{}", self.id)) WebhookId::new(format!("{}", self.id))
} }
pub fn url(&self) -> server::WebhookUrl { pub fn url(&self) -> server::ListenUrl {
server::WebhookUrl::new(self.config.url.clone()) server::ListenUrl::new(self.config.url.clone())
} }
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]

View file

@ -2,7 +2,7 @@
use crate::{Github, GithubState, GithubStatus}; use crate::{Github, GithubState, GithubStatus};
use git_next_core::{ use git_next_core::{
git::{self, forge::commit::Status, ForgeLike}, git::{self, forge::commit::Status, ForgeLike},
server::{InboundWebhook, WebhookUrl}, server::ListenUrl,
webhook::{self, forge_notification::Body}, webhook::{self, forge_notification::Body},
ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias,
RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId, RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId,
@ -158,7 +158,7 @@ mod github {
#[tokio::test] #[tokio::test]
async fn should_return_a_list_of_matching_webhooks() { async fn should_return_a_list_of_matching_webhooks() {
let repo_details = given::repo_details(); let repo_details = given::repo_details();
let webhook_url = given::any_webhook_url(); let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let hook_id_1 = given::a_github_webhook_id(); let hook_id_1 = given::a_github_webhook_id();
@ -174,8 +174,8 @@ mod github {
with::get_webhooks_by_page( with::get_webhooks_by_page(
1, 1,
&[ &[
with::ReturnedWebhook::new(hook_id_1, &webhook_url), with::ReturnedWebhook::new(hook_id_1, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_2, &webhook_url), with::ReturnedWebhook::new(hook_id_2, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()), with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()),
], ],
&mut args, &mut args,
@ -185,7 +185,7 @@ mod github {
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Ok(result) = forge.list_webhooks(&webhook_url).await); let_assert!(Ok(result) = forge.list_webhooks(&repo_listen_url).await);
assert_eq!( assert_eq!(
result, result,
vec![ vec![
@ -198,7 +198,7 @@ mod github {
#[tokio::test] #[tokio::test]
async fn should_return_any_network_error() { async fn should_return_any_network_error() {
let repo_details = given::repo_details(); let repo_details = given::repo_details();
let webhook_url = given::a_webhook_url(&given::a_forge_alias(), &given::a_repo_alias()); let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let mut net = given::a_network(); let mut net = given::a_network();
@ -209,7 +209,7 @@ mod github {
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Err(_) = forge.list_webhooks(&webhook_url).await); let_assert!(Err(_) = forge.list_webhooks(&repo_listen_url).await);
} }
} }
@ -271,8 +271,7 @@ mod github {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let mut net = given::a_network(); let mut net = given::a_network();
@ -288,14 +287,14 @@ mod github {
net.add_post_response( net.add_post_response(
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(),
StatusCode::OK, StatusCode::OK,
json!({"id": webhook_id, "config":{"url": webhook_url}}) json!({"id": webhook_id, "config":{"url": repo_listen_url.to_string()}})
.to_string() .to_string()
.as_str(), .as_str(),
); );
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Ok(registered_webhook) = forge.register_webhook(&webhook_url).await); let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!( assert_eq!(
registered_webhook.id(), registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}")) &WebhookId::new(format!("{webhook_id}"))
@ -310,11 +309,10 @@ mod github {
repo_details.repo_config.is_none(), repo_details.repo_config.is_none(),
"repo_details needs to NOT have repo_config for this test" "repo_details needs to NOT have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let net = given::a_network(); let net = given::a_network();
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::NoRepoConfig), matches!(err, git::forge::webhook::Error::NoRepoConfig),
"{err:?}" "{err:?}"
@ -328,8 +326,7 @@ mod github {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let mut net = given::a_network(); let mut net = given::a_network();
@ -338,8 +335,8 @@ mod github {
hostname, hostname,
repo_path, repo_path,
}; };
let hook1 = with::ReturnedWebhook::new(given::a_github_webhook_id(), &webhook_url); let hook1 = with::ReturnedWebhook::new(given::a_github_webhook_id(), &repo_listen_url);
let hook2 = with::ReturnedWebhook::new(given::a_github_webhook_id(), &webhook_url); let hook2 = with::ReturnedWebhook::new(given::a_github_webhook_id(), &repo_listen_url);
let hook3 = let hook3 =
with::ReturnedWebhook::new(given::a_github_webhook_id(), &given::any_webhook_url()); with::ReturnedWebhook::new(given::a_github_webhook_id(), &given::any_webhook_url());
let hooks = [hook1, hook2, hook3]; let hooks = [hook1, hook2, hook3];
@ -354,14 +351,14 @@ mod github {
net.add_post_response( net.add_post_response(
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(),
StatusCode::OK, StatusCode::OK,
json!({"id": webhook_id, "config":{"url": webhook_url}}) json!({"id": webhook_id, "config":{"url": repo_listen_url.to_string()}})
.to_string() .to_string()
.as_str(), .as_str(),
); );
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Ok(registered_webhook) = forge.register_webhook(&webhook_url).await); let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!( assert_eq!(
registered_webhook.id(), registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}")) &WebhookId::new(format!("{webhook_id}"))
@ -375,8 +372,7 @@ mod github {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let mut net = given::a_network(); let mut net = given::a_network();
@ -396,7 +392,7 @@ mod github {
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}" "{err:?}"
@ -410,8 +406,7 @@ mod github {
repo_details.repo_config.is_some(), repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test" "repo_details needs to have repo_config for this test"
); );
let webhook_url = let repo_listen_url = given::a_repo_listen_url(&repo_details);
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let mut net = given::a_network(); let mut net = given::a_network();
@ -430,7 +425,7 @@ mod github {
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}" "{err:?}"
@ -439,6 +434,8 @@ mod github {
} }
pub mod with { pub mod with {
use git_next_core::server::RepoListenUrl;
use super::*; use super::*;
pub fn get_webhooks_by_page( pub fn get_webhooks_by_page(
@ -476,21 +473,26 @@ mod github {
pub config: Config, pub config: Config,
} }
impl ReturnedWebhook { impl ReturnedWebhook {
pub fn new(id: i64, url: &WebhookUrl) -> Self { pub fn new(id: i64, url: &RepoListenUrl) -> Self {
Self { Self {
id, id,
config: Config { url: url.clone() }, config: Config {
url: url.to_string(),
},
} }
} }
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Config { pub struct Config {
pub url: WebhookUrl, pub url: String,
} }
} }
mod given { mod given {
use git::RepoDetails;
use git_next_core::server::RepoListenUrl;
use super::*; use super::*;
pub fn commit_states( pub fn commit_states(
@ -615,11 +617,15 @@ mod github {
kxio::network::MockNetwork::new() kxio::network::MockNetwork::new()
} }
pub fn a_webhook_url(forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> WebhookUrl { pub fn a_repo_listen_url(repo_details: &RepoDetails) -> RepoListenUrl {
InboundWebhook::new(a_name()).url(forge_alias, repo_alias) RepoListenUrl::new((
ListenUrl::new(a_name()),
repo_details.forge.forge_alias().clone(),
repo_details.repo_alias.clone(),
))
} }
pub fn any_webhook_url() -> WebhookUrl { pub fn any_webhook_url() -> RepoListenUrl {
given::a_webhook_url(&given::a_forge_alias(), &given::a_repo_alias()) given::a_repo_listen_url(&given::repo_details())
} }
pub fn a_name() -> String { pub fn a_name() -> String {

View file

@ -1,13 +1,13 @@
// //
use crate as github; use crate as github;
use git_next_core::{git, server, WebhookId}; use git_next_core::{git, server::RepoListenUrl, WebhookId};
use kxio::network; use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#list-repository-webhooks // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#list-repository-webhooks
pub async fn list( pub async fn list(
github: &github::Github, github: &github::Github,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<Vec<WebhookId>> { ) -> git::forge::webhook::Result<Vec<WebhookId>> {
let mut ids: Vec<WebhookId> = vec![]; let mut ids: Vec<WebhookId> = vec![];
let repo_details = &github.repo_details; let repo_details = &github.repo_details;
@ -39,7 +39,11 @@ pub async fn list(
return Ok(ids); return Ok(ids);
} }
for hook in list { for hook in list {
if hook.url().as_ref().starts_with(webhook_url.as_ref()) { if hook
.url()
.as_ref()
.starts_with(&repo_listen_url.to_string())
{
ids.push(hook.id()); ids.push(hook.id());
} }
} }

View file

@ -1,13 +1,13 @@
// //
use crate::{self as github, webhook}; use crate::{self as github, webhook};
use git_next_core::{git, server, RegisteredWebhook, WebhookAuth, WebhookId}; use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use kxio::network; use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook
pub async fn register( pub async fn register(
github: &github::Github, github: &github::Github,
webhook_url: &server::WebhookUrl, repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<RegisteredWebhook> { ) -> git::forge::webhook::Result<RegisteredWebhook> {
let repo_details = &github.repo_details; let repo_details = &github.repo_details;
if repo_details.repo_config.is_none() { if repo_details.repo_config.is_none() {
@ -15,7 +15,7 @@ pub async fn register(
}; };
// remove any lingering webhooks for the same URL // remove any lingering webhooks for the same URL
let existing_webhook_ids = webhook::list(github, webhook_url).await?; let existing_webhook_ids = webhook::list(github, repo_listen_url).await?;
for webhook_id in existing_webhook_ids { for webhook_id in existing_webhook_ids {
webhook::unregister(github, &webhook_id).await?; webhook::unregister(github, &webhook_id).await?;
} }
@ -35,7 +35,7 @@ pub async fn register(
"active": true, "active": true,
"events": ["push"], "events": ["push"],
"config": { "config": {
"url": webhook_url.as_ref(), "url": repo_listen_url.as_ref(),
"content_type": "json", "content_type": "json",
"secret": authorisation.to_string(), "secret": authorisation.to_string(),
"insecure_ssl": "0", "insecure_ssl": "0",