WIP: feat!: listen & shout config
All checks were successful
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

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
parent 8df7600053
commit 8c10fd94a6
22 changed files with 293 additions and 213 deletions

View file

@ -87,7 +87,9 @@ happen to have those same names.
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,
(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
- **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.
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
The HTTPS URL for forges to send webhooks to.
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
@ -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
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
@ -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
untested and undefined.
## Notifications
## Webhook Notifications
`git-next` can send a number of notification to the user when intervention is required.
Currently, only WebHooks are supported.
Webhooks are sent using the Standard Webhooks format. That means all POST messages have
the following headers:
When sending a Webhook Notification to a user they are sent using the
Standard Webhooks format. That means all POST messages have the
following headers:
- `Webhook-Id`
- `Webhook-Signature`

View file

@ -9,7 +9,7 @@ use crate::repo::{
notify_user, RepoActor,
};
use git_next_core::git::UserNotification;
use git_next_core::{git::UserNotification, server::ListenUrl};
impl Handler<RegisterWebhook> for RepoActor {
type Result = ();
@ -18,14 +18,14 @@ impl Handler<RegisterWebhook> for RepoActor {
if self.webhook_id.is_none() {
let forge_alias = self.repo_details.forge.forge_alias().clone();
let repo_alias = self.repo_details.repo_alias.clone();
let webhook_url = self.webhook.url(&forge_alias, &repo_alias);
let repo_listen_url = self.listen_url.repo_url(forge_alias, repo_alias);
let forge = self.forge.duplicate();
let addr = ctx.address();
let notify_user_recipient = self.notify_user_recipient.clone();
let log = self.log.clone();
debug!("registering webhook");
async move {
match forge.register_webhook(&webhook_url).await {
match forge.register_webhook(&repo_listen_url).await {
Ok(registered_webhook) => {
debug!(?registered_webhook, "");
do_send(

View file

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

View file

@ -1,3 +1,5 @@
use git_next_core::server::{Http, Listen, ListenUrl, RepoListenUrl};
//
use super::*;
@ -50,8 +52,12 @@ pub fn a_network() -> kxio::network::MockNetwork {
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 a_webhook_url(forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> RepoListenUrl {
a_listen_url().repo_url(forge_alias, repo_alias)
}
pub fn a_listen_url() -> ListenUrl {
ListenUrl::new(a_name())
}
pub fn a_name() -> String {
@ -168,8 +174,12 @@ pub fn a_message_token() -> MessageToken {
MessageToken::default()
}
pub fn a_webhook(url: &WebhookUrl) -> InboundWebhook {
InboundWebhook::new(url.clone().into())
pub fn a_listen() -> Listen {
Listen::new(Http::new("0.0.0.0".to_string(), 8080), a_listen_url())
}
pub fn a_webhook(url: &ListenUrl) -> String {
url.clone().into()
}
pub fn a_forge() -> Box<MockForgeLike> {
@ -182,10 +192,7 @@ pub fn a_repo_actor(
forge: Box<dyn ForgeLike>,
net: kxio::network::Network,
) -> (RepoActor, RepoActorLog) {
let forge_alias = repo_details.forge.forge_alias();
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 listen_url = given::a_listen_url();
let generation = Generation::default();
let log = RepoActorLog::default();
let actors_log = log.clone();
@ -193,7 +200,7 @@ pub fn a_repo_actor(
RepoActor::new(
repo_details,
forge,
webhook,
listen_url,
generation,
net,
repository_factory,

View file

@ -21,7 +21,7 @@ use git_next_core::{
Commit, ForgeLike, Generation, MockForgeLike, RepoDetails,
},
message,
server::{InboundWebhook, WebhookUrl},
server::ListenUrl,
webhook::{forge_notification::Body, Push},
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname,
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 {
return;
};
let notification_config = server_config.notification().clone();
let shout_config = server_config.shout().clone();
let net = self.net.clone();
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;
}
}

View file

@ -11,7 +11,7 @@ impl Handler<ReceiveServerConfig> for ServerActor {
#[allow(clippy::cognitive_complexity)]
fn handle(&mut self, msg: ReceiveServerConfig, ctx: &mut Self::Context) -> Self::Result {
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");
};
@ -19,7 +19,7 @@ impl Handler<ReceiveServerConfig> for ServerActor {
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 '/'");
}

View file

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

View file

@ -20,7 +20,7 @@ use crate::{
use git_next_core::{
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
server::{self, InboundWebhook, ServerConfig, ServerStorage},
server::{self, ListenUrl, ServerConfig, ServerStorage},
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
};
@ -113,7 +113,7 @@ impl ServerActor {
forge_config: &ForgeConfig,
forge_name: ForgeAlias,
server_storage: &ServerStorage,
webhook: &InboundWebhook,
listen_url: &ListenUrl,
notify_user_recipient: Recipient<NotifyUser>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
let span =
@ -122,7 +122,8 @@ impl ServerActor {
let _guard = span.enter();
tracing::info!("Creating Forge");
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() {
let forge_repo = creator((
repo_alias,
@ -143,17 +144,16 @@ impl ServerActor {
forge_name: ForgeAlias,
forge_config: ForgeConfig,
server_storage: &ServerStorage,
webhook: &InboundWebhook,
listen_url: &ListenUrl,
) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
) -> (ForgeAlias, RepoAlias, RepoActor) {
let server_storage = server_storage.clone();
let webhook = webhook.clone();
let listen_url = listen_url.clone();
let net = self.net.clone();
let repository_factory = self.repository_factory.duplicate();
let generation = self.generation;
let sleep_duration = self.sleep_duration;
// let notify_user_recipient = server_addr.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 _guard = span.enter();
@ -185,7 +185,7 @@ impl ServerActor {
let actor = RepoActor::new(
repo_details,
forge,
webhook.clone(),
listen_url.clone(),
generation,
net.clone(),
repository_factory.duplicate(),

View file

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

View file

@ -1,11 +1,12 @@
[http] # where to listen for incoming updates from forges
addr = "0.0.0.0"
port = 8080
[listen]
# The address and port to listen to for incoming webhooks from forges.
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
[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" }
[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
# 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" }
# 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

@ -3,12 +3,15 @@
use std::{
collections::BTreeMap,
net::SocketAddr,
ops::Deref,
path::{Path, PathBuf},
str::FromStr,
};
use derive_more::Display;
use kxio::fs::FileSystem;
use secrecy::Secret;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
@ -44,9 +47,8 @@ type Result<T> = core::result::Result<T, Error>;
derive_more::Constructor,
)]
pub struct ServerConfig {
http: Http,
webhook: InboundWebhook,
notification: Notification,
listen: Listen,
shout: Shout,
storage: ServerStorage,
pub forge: BTreeMap<String, ForgeConfig>,
}
@ -69,16 +71,75 @@ impl ServerConfig {
&self.storage
}
pub const fn notification(&self) -> &Notification {
&self.notification
pub const fn shout(&self) -> &Shout {
&self.shout
}
pub const fn inbound_webhook(&self) -> &InboundWebhook {
&self.webhook
pub const fn listen(&self) -> &Listen {
&self.listen
}
pub fn http(&self) -> Result<SocketAddr> {
self.http.socket_addr()
pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
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), Serialize: "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
#[derive(
Clone,
@ -164,6 +196,7 @@ impl ServerStorage {
#[derive(
Clone,
Debug,
Default,
derive_more::From,
PartialEq,
Eq,
@ -172,13 +205,10 @@ impl ServerStorage {
derive_more::AsRef,
serde::Deserialize,
)]
pub struct Notification {
pub struct Shout {
webhook: Option<OutboundWebhook>,
}
impl Notification {
pub const fn none() -> Self {
Self { webhook: None }
}
impl Shout {
pub const fn new_webhook(webhook: OutboundWebhook) -> Self {
Self {
webhook: Some(webhook),
@ -197,11 +227,6 @@ impl Notification {
self.webhook.clone().map(|x| x.secret).map(Secret::new)
}
}
impl Default for Notification {
fn default() -> Self {
Self::none()
}
}
#[derive(
Clone,

View file

@ -7,7 +7,6 @@ use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::server::Http;
use crate::server::InboundWebhook;
use crate::server::ServerConfig;
use crate::server::ServerStorage;
use crate::webhook::push::Branch;
@ -448,6 +447,8 @@ mod server {
use super::*;
mod load {
use std::ops::Deref;
use super::*;
#[test]
@ -465,12 +466,12 @@ mod server {
server_config: &ServerConfig,
fs: &kxio::fs::FileSystem,
) -> TestResult {
let http = &server_config.http()?;
let http = &server_config.listen_socket_addr()?;
let http_addr = http.ip();
let http_port = server_config.http()?.port();
let webhook_url = server_config.inbound_webhook().base_url();
let http_port = server_config.listen_socket_addr()?.port();
let webhook_url = server_config.listen().url();
let storage_path = server_config.storage().path();
let notification = &server_config.notification();
let notification = &server_config.shout();
let notification_webhook_url = notification.webhook_url().unwrap_or_default();
let notification_webhook_secret = notification
.webhook_secret()
@ -697,7 +698,7 @@ mod push {
}
mod given {
use crate::server::{Notification, OutboundWebhook};
use crate::server::{Listen, ListenUrl, OutboundWebhook, Shout};
use super::*;
use rand::Rng as _;
@ -720,8 +721,7 @@ mod given {
}
pub fn a_server_config() -> ServerConfig {
ServerConfig::new(
an_http(),
an_inbound_webhook(),
a_listen(),
a_notification_config(),
a_server_storage(),
some_forge_configs(),
@ -743,8 +743,11 @@ mod given {
pub fn a_port() -> u16 {
rand::thread_rng().gen()
}
pub fn an_inbound_webhook() -> InboundWebhook {
InboundWebhook::new(a_name())
pub fn a_listen() -> Listen {
Listen::new(an_http(), a_listen_url())
}
pub fn a_listen_url() -> ListenUrl {
ListenUrl::new(a_name())
}
pub fn an_outbound_webhook() -> OutboundWebhook {
OutboundWebhook::new(a_name(), a_name())
@ -752,8 +755,8 @@ mod given {
pub fn a_server_storage() -> ServerStorage {
ServerStorage::new(a_name().into())
}
pub fn a_notification_config() -> Notification {
Notification::new_webhook(an_outbound_webhook())
pub fn a_notification_config() -> Shout {
Shout::new_webhook(an_outbound_webhook())
}
pub fn some_forge_configs() -> BTreeMap<String, ForgeConfig> {
[(a_name(), a_forge_config())].into()

View file

@ -1,6 +1,8 @@
//
use crate::{
git, server::WebhookUrl, webhook, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
git,
server::{ForgeWebhookUrl, ListenUrl, RepoListenUrl},
webhook, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
};
#[mockall::automock]
@ -29,7 +31,10 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
async fn commit_status(&self, commit: &git::Commit) -> git::forge::commit::Status;
// 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
async fn unregister_webhook(&self, webhook: &WebhookId) -> git::forge::webhook::Result<()>;
@ -37,6 +42,6 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
// Registers a webhook
async fn register_webhook(
&self,
webhook_url: &WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<RegisteredWebhook>;
}

View file

@ -5,8 +5,10 @@ mod tests;
mod webhook;
use git_next_core::{
self as core, git, git::forge::commit::Status, server, ForgeNotification, RegisteredWebhook,
WebhookAuth, WebhookId,
self as core,
git::{self, forge::commit::Status},
server::{self, RepoListenUrl},
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
};
use kxio::network::{self, Network};
@ -93,9 +95,9 @@ impl git::ForgeLike for ForgeJo {
async fn list_webhooks(
&self,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> 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<()> {
@ -104,9 +106,9 @@ impl git::ForgeLike for ForgeJo {
async fn register_webhook(
&self,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> 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 git_next_core::{
git::{self, forge::commit::Status, ForgeLike as _},
server::{InboundWebhook, WebhookUrl},
server::ListenUrl,
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias,
RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId,
};
@ -198,7 +198,7 @@ mod forgejo {
async fn should_return_a_list_of_matching_webhooks() {
let fs = given::a_filesystem();
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 repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
@ -217,9 +217,9 @@ mod forgejo {
with::get_webhooks_by_page(
1,
&[
with::ReturnedWebhook::new(hook_id_1, &webhook_url),
with::ReturnedWebhook::new(hook_id_2, &webhook_url),
with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()),
with::ReturnedWebhook::new(hook_id_1, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_2, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_3, &given::a_repo_listen_url(&repo_details)),
],
&mut args,
);
@ -228,7 +228,7 @@ mod forgejo {
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!(
result,
vec![
@ -242,7 +242,7 @@ mod forgejo {
async fn should_return_any_network_error() {
let fs = given::a_filesystem();
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 repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
@ -256,7 +256,7 @@ mod forgejo {
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 needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
@ -352,7 +351,7 @@ mod forgejo {
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!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
@ -368,11 +367,10 @@ mod forgejo {
repo_details.repo_config.is_none(),
"repo_details needs to NOT have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let net = given::a_network();
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!(
matches!(err, git::forge::webhook::Error::NoRepoConfig),
"{err:?}"
@ -387,8 +385,7 @@ mod forgejo {
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
@ -400,11 +397,11 @@ mod forgejo {
repo_path,
token,
};
let hook1 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &webhook_url);
let hook2 = 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(), &repo_listen_url);
let hook3 = with::ReturnedWebhook::new(
given::a_forgejo_webhook_id(),
&given::any_webhook_url(),
&given::a_repo_listen_url(&repo_details),
);
let hooks = [hook1, hook2, hook3];
@ -424,7 +421,7 @@ mod forgejo {
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!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
@ -439,8 +436,7 @@ mod forgejo {
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
@ -464,7 +460,7 @@ mod forgejo {
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!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
@ -479,8 +475,7 @@ mod forgejo {
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
@ -503,7 +498,7 @@ mod forgejo {
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!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
@ -511,6 +506,8 @@ mod forgejo {
}
}
mod with {
use git_next_core::server::RepoListenUrl;
use super::*;
pub fn get_webhooks_by_page(
@ -556,7 +553,7 @@ mod forgejo {
pub config: Config,
}
impl ReturnedWebhook {
pub fn new(id: i64, url: &WebhookUrl) -> Self {
pub fn new(id: i64, url: &RepoListenUrl) -> Self {
Self {
id,
config: Config { url: url.clone() },
@ -565,11 +562,14 @@ mod forgejo {
}
#[derive(Debug, Serialize)]
pub struct Config {
pub url: WebhookUrl,
pub url: RepoListenUrl,
}
}
mod given {
use git::RepoDetails;
use git_next_core::server::RepoListenUrl;
use super::*;
pub fn a_commit_state(
@ -674,11 +674,11 @@ mod forgejo {
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 a_webhook_url(forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> ListenUrl {
ListenUrl::new(format!("{}/{}/{}", a_name(), forge_alias, repo_alias))
}
pub fn any_webhook_url() -> WebhookUrl {
pub fn any_webhook_url() -> ListenUrl {
a_webhook_url(&a_forge_alias(), &a_repo_alias())
}
@ -765,5 +765,16 @@ mod forgejo {
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,9 @@
//
use git_next_core::{git, server::WebhookUrl, WebhookId};
use git_next_core::{
git,
server::{ListenUrl, RepoListenUrl},
WebhookId,
};
use kxio::network;
@ -7,7 +11,7 @@ use crate::webhook::Hook;
pub async fn list(
repo_details: &git::RepoDetails,
webhook_url: &WebhookUrl,
repo_listen_url: &RepoListenUrl,
net: &network::Network,
) -> git::forge::webhook::Result<Vec<WebhookId>> {
let mut ids: Vec<WebhookId> = vec![];
@ -38,7 +42,7 @@ pub async fn list(
}
for hook in list {
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());
}
}

View file

@ -1,3 +1,4 @@
use git_next_core::server::RepoListenUrl;
//
use git_next_core::{git, server, RegisteredWebhook, WebhookAuth, WebhookId};
@ -10,7 +11,7 @@ use crate::webhook::Hook;
#[tracing::instrument(skip_all)]
pub async fn register(
repo_details: &git::RepoDetails,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
net: &network::Network,
) -> git::forge::webhook::Result<RegisteredWebhook> {
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
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 {
webhook::unregister(&webhook_id, repo_details, net).await?;
}
@ -30,7 +31,6 @@ pub async fn register(
let url = network::NetUrl::new(format!(
"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 authorisation = WebhookAuth::generate();
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()),
"config": {
"content_type": "json",
"url": format!("{}/{}", webhook_url.as_ref(), repo_alias),
"url": repo_listen_url.to_string(),
},
"events": [ "push" ],
"type": "forgejo"

View file

@ -9,7 +9,8 @@ use crate as github;
use git_next_core::{
self as core,
git::{self, forge::commit::Status},
server, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
server::{self, RepoListenUrl},
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
};
use derive_more::Constructor;
@ -57,9 +58,9 @@ impl git::ForgeLike for Github {
async fn list_webhooks(
&self,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> 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<()> {
@ -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
async fn register_webhook(
&self,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> 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 {
WebhookId::new(format!("{}", self.id))
}
pub fn url(&self) -> server::WebhookUrl {
server::WebhookUrl::new(self.config.url.clone())
pub fn url(&self) -> server::ListenUrl {
server::ListenUrl::new(self.config.url.clone())
}
}
#[derive(Debug, serde::Deserialize)]

View file

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

View file

@ -1,13 +1,17 @@
//
use crate as github;
use git_next_core::{git, server, WebhookId};
use git_next_core::{
git,
server::{self, RepoListenUrl},
WebhookId,
};
use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#list-repository-webhooks
pub async fn list(
github: &github::Github,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<Vec<WebhookId>> {
let mut ids: Vec<WebhookId> = vec![];
let repo_details = &github.repo_details;
@ -39,7 +43,11 @@ pub async fn list(
return Ok(ids);
}
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());
}
}

View file

@ -1,13 +1,13 @@
//
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;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook
pub async fn register(
github: &github::Github,
webhook_url: &server::WebhookUrl,
repo_listen_url: &RepoListenUrl,
) -> git::forge::webhook::Result<RegisteredWebhook> {
let repo_details = &github.repo_details;
if repo_details.repo_config.is_none() {
@ -15,7 +15,7 @@ pub async fn register(
};
// 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 {
webhook::unregister(github, &webhook_id).await?;
}
@ -35,7 +35,7 @@ pub async fn register(
"active": true,
"events": ["push"],
"config": {
"url": webhook_url.as_ref(),
"url": repo_listen_url.as_ref(),
"content_type": "json",
"secret": authorisation.to_string(),
"insecure_ssl": "0",