Compare commits

..

No commits in common. "1163d2a3f3133faa1604e40916a5bda77e404de5" and "bddff51ab3bb482f507454d23c7227e0d5bb1329" have entirely different histories.

4 changed files with 59 additions and 180 deletions

View File

@ -2,15 +2,7 @@ FROM rust:1.72-slim-bullseye AS builder
WORKDIR /bebot-build WORKDIR /bebot-build
# Build and cache dependencies COPY . ./
COPY Cargo.toml Cargo.lock ./
RUN mkdir -p src && echo 'fn main() {}' > src/main.rs
RUN cargo build --release
# Build and link our app
RUN rm -rf src
COPY src ./src
RUN touch src/main.rs
RUN cargo build --release RUN cargo build --release
FROM debian:bookworm-slim FROM debian:bookworm-slim

View File

@ -1,58 +0,0 @@
#!/usr/bin/env python3
import re
import os
import requests
import secrets
import sys
def die(s):
print(s, file=sys.stderr)
sys.exit(1)
gitlab_token = os.getenv('GITLAB_TOKEN')
if gitlab_token is None:
die("GITLAB_TOKEN must be set in the environment")
if len(sys.argv) < 4:
die("Usage: {} GITLAB_INSTANCE HOOK_URL_BASE NAMESPACE/REPO\n\nExample: {} gitlab.example.com https://bot.example.com/bebot stuff/myrepo")
gitlab_instance = sys.argv[1]
hook_url = f"{sys.argv[2]}/hooks/gitlab"
repo_name = sys.argv[3]
gl_base = 'https://{}/api/v4/projects/{}/hooks'.format(
gitlab_instance,
re.sub('/', '%2F', repo_name)
)
headers = {
'authorization': 'Bearer {}'.format(gitlab_token),
}
payload = {
"merge_requests_events": True,
"pipeline_events": True,
"push_events": True,
"tag_push_events": True,
}
resp = requests.get(gl_base, headers=headers)
if resp.status_code != 200:
resp.raise_for_status()
existing = resp.json()
for hook in existing:
if hook['url'] == hook_url:
upd_url = '{}/{}'.format(gl_base, hook['id'])
resp = requests.put(upd_url, headers=headers, json=payload)
print("Updated existing hook")
sys.exit(0)
token = secrets.token_urlsafe(32)
payload["url"] = hook_url
payload["token"] = token
resp = requests.post(gl_base, headers=headers, json=payload)
resp.raise_for_status()
print(' "{}/{}":'.format(gitlab_instance, repo_name))
print(' token: "{}"'.format(token))

View File

@ -2,7 +2,8 @@ pub trait GitlabEventExt {
fn project(&self) -> &Project; fn project(&self) -> &Project;
fn r#ref(&self) -> &str; fn r#ref(&self) -> &str;
fn user(&self) -> &str; fn user(&self) -> &str;
fn titles(&self) -> Vec<String>; fn url(&self) -> String;
fn title(&self) -> String;
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -13,7 +14,9 @@ pub struct User {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
pub description: String,
pub web_url: String, pub web_url: String,
pub avatar_url: Option<String>,
pub namespace: String, pub namespace: String,
pub path_with_namespace: String, pub path_with_namespace: String,
pub default_branch: String, pub default_branch: String,
@ -23,6 +26,7 @@ pub struct Project {
pub struct Repository { pub struct Repository {
pub name: String, pub name: String,
pub url: String, pub url: String,
pub description: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -69,7 +73,6 @@ impl MergeRequestAction {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct MergeRequestObjectAttributes { pub struct MergeRequestObjectAttributes {
pub iid: u64,
pub target_branch: String, pub target_branch: String,
pub source_branch: String, pub source_branch: String,
pub title: String, pub title: String,
@ -158,7 +161,7 @@ pub enum GitlabEvent {
#[serde(rename = "pipeline")] #[serde(rename = "pipeline")]
Pipeline { Pipeline {
object_attributes: PipelineObjectAttributes, object_attributes: PipelineObjectAttributes,
merge_request: Option<PipelineMergeRequest>, merge_request: PipelineMergeRequest,
user: User, user: User,
project: Project, project: Project,
}, },
@ -192,101 +195,54 @@ impl GitlabEventExt for GitlabEvent {
} }
} }
fn titles(&self) -> Vec<String> { fn url(&self) -> String {
let url = match self {
GitlabEvent::Push { after, project, .. } => format!("{}/-/commits/{}", project.web_url, after),
GitlabEvent::TagPush {
r#ref,
checkout_sha,
project,
..
} => {
let refname = r#ref.split('/').into_iter().last().unwrap_or(checkout_sha);
format!("{}/-/tags/{}", project.web_url, refname)
}
GitlabEvent::MergeRequest { object_attributes, .. } => object_attributes.url.clone(),
GitlabEvent::Pipeline { object_attributes, .. } => object_attributes.url.clone(),
};
url.replace("http://", "https://").to_string()
}
fn title(&self) -> String {
fn find_commit<'a>(commits: &'a Vec<Commit>, sha: &str) -> Option<&'a Commit> { fn find_commit<'a>(commits: &'a Vec<Commit>, sha: &str) -> Option<&'a Commit> {
commits.iter().find(|commit| commit.id == sha) commits.iter().find(|commit| commit.id == sha)
} }
match self { match self {
GitlabEvent::Push { GitlabEvent::Push { after, commits, .. } => find_commit(commits, &after)
after, .map(|commit| commit.title.clone())
project, .unwrap_or_else(|| "New commit(s) pushed".to_string()),
commits,
..
} => {
const MAX_COMMITS: usize = 15; // TODO: make configurable
commits.iter().fold(Vec::new(), |mut titles, commit| {
if titles.len() < MAX_COMMITS {
titles.push(format!("[**pushed** {}]({})", commit.title, commit.url));
if titles.len() == MAX_COMMITS {
titles.push(format!(
"[**pushed** {} more commits]({}/-/compare/{}...{})",
commits.len() - MAX_COMMITS,
project.web_url,
commit.id,
after
));
}
}
titles
})
}
GitlabEvent::TagPush { GitlabEvent::TagPush {
r#ref, checkout_sha, commits, ..
checkout_sha, } => find_commit(commits, &checkout_sha)
project, .map(|commit| commit.title.clone())
commits, .unwrap_or_else(|| "New tag pushed".to_string()),
..
} => {
let title = format!(
"**tagged** {}",
find_commit(commits, &checkout_sha)
.map(|commit| &commit.title)
.unwrap_or(checkout_sha)
);
let url = format!("{}/-/tags/{}", project.web_url, parse_ref(r#ref));
vec![markdown_link(&title, &url)]
}
GitlabEvent::MergeRequest { object_attributes, .. } => { GitlabEvent::MergeRequest { object_attributes, .. } => {
let title = format!( format!("MR {}: {}", object_attributes.action.as_str(), object_attributes.title)
"MR !{} **{}**: {}",
object_attributes.iid,
object_attributes.action.as_str(),
object_attributes.title
);
vec![markdown_link(&title, &object_attributes.url)]
} }
GitlabEvent::Pipeline { GitlabEvent::Pipeline {
object_attributes, object_attributes,
merge_request, merge_request,
.. ..
} => { } => {
let title = object_attributes let title = object_attributes.name.as_ref().unwrap_or(&merge_request.title);
.name format!("Pipeline {}: {}", object_attributes.status.as_str(), title)
.as_ref()
.map(|n| n.clone())
.or(merge_request.as_ref().map(|mr| mr.title.clone()))
.iter()
.fold(
format!("Pipeline **{}**", object_attributes.status.as_str()),
|accum, title| format!("{}: {}", accum, title),
);
vec![markdown_link(&title, &object_attributes.url)]
} }
} }
} }
} }
#[inline]
fn markdown_link(title: &String, url: &String) -> String {
format!("[{}]({})", title, url)
}
pub fn parse_ref(r#ref: &str) -> String {
if r#ref.starts_with("refs/") {
let parts = r#ref.split('/').collect::<Vec<_>>();
if parts.len() > 2 {
parts.into_iter().skip(2).collect::<Vec<_>>().join("/").to_string()
} else {
r#ref.to_string()
}
} else {
r#ref.to_string()
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -20,7 +20,6 @@ use matrix_sdk::{
ruma::{events::room::message::RoomMessageEventContent, OwnedRoomOrAliasId}, ruma::{events::room::message::RoomMessageEventContent, OwnedRoomOrAliasId},
BaseRoom, Client, BaseRoom, Client,
}; };
use tokio::sync::mpsc;
use warp::Filter; use warp::Filter;
use crate::event::{MergeRequestAction, PipelineStatus}; use crate::event::{MergeRequestAction, PipelineStatus};
@ -105,22 +104,22 @@ async fn ensure_matrix_room_joined(matrix_client: &Client, room_id: &OwnedRoomOr
room.ok_or_else(|| anyhow!("Unable to join room {}", room_id)) room.ok_or_else(|| anyhow!("Unable to join room {}", room_id))
} }
fn build_gitlab_messages(event: &GitlabEvent) -> Vec<String> { fn build_gitlab_message(event: &GitlabEvent) -> String {
let project = event.project(); let project = event.project();
let refname = event::parse_ref(event.r#ref()); let refname = event
event .r#ref()
.titles() .split('/')
.iter() .last()
.map(|title| { .unwrap_or_else(|| event.r#ref())
.to_string();
format!( format!(
"\\[{}\\] `{}` *{}* {}", "*{}* {} **{}** [{}]({})",
project.path_with_namespace, project.path_with_namespace,
refname, refname,
event.user(), event.user(),
title, event.title(),
event.url()
) )
})
.collect()
} }
async fn handle_gitlab_event( async fn handle_gitlab_event(
@ -139,11 +138,10 @@ async fn handle_gitlab_event(
} }
let room = ensure_matrix_room_joined(matrix_client, room_id).await?; let room = ensure_matrix_room_joined(matrix_client, room_id).await?;
for msg in build_gitlab_messages(&event) { let msg = build_gitlab_message(&event);
debug!("Sending message to {}: {}", room_id, msg); debug!("Sending message to {}: {}", room_id, msg);
let msg_content = RoomMessageEventContent::text_markdown(&msg); let msg_content = RoomMessageEventContent::text_markdown(&msg);
room.send(msg_content, None).await?; room.send(msg_content, None).await?;
}
Ok(()) Ok(())
} }
@ -163,15 +161,6 @@ async fn run() -> anyhow::Result<()> {
let matrix_client = matrix_connect(&config).await.context("Failed to connect to Matrix")?; let matrix_client = matrix_connect(&config).await.context("Failed to connect to Matrix")?;
let (event_tx, mut event_rx) = mpsc::channel::<(GitlabEvent, OwnedRoomOrAliasId)>(100);
tokio::spawn(async move {
while let Some((event, room)) = event_rx.recv().await {
if let Err(err) = handle_gitlab_event(event, &room, &matrix_client).await {
warn!("Failed to handle payload: {}", err);
}
}
});
let gitlab_root_path = if let Some(url_prefix) = config.url_prefix.as_ref() { let gitlab_root_path = if let Some(url_prefix) = config.url_prefix.as_ref() {
url_prefix.split('/').fold(warp::any().boxed(), |last, segment| { url_prefix.split('/').fold(warp::any().boxed(), |last, segment| {
if segment.is_empty() { if segment.is_empty() {
@ -191,7 +180,7 @@ async fn run() -> anyhow::Result<()> {
.and(warp::body::json()) .and(warp::body::json())
.then(move |token: String, event: event::GitlabEvent| { .then(move |token: String, event: event::GitlabEvent| {
let config = Arc::clone(&config); let config = Arc::clone(&config);
let event_tx = event_tx.clone(); let matrix_client = matrix_client.clone();
async move { async move {
let project = event.project(); let project = event.project();
@ -203,8 +192,8 @@ async fn run() -> anyhow::Result<()> {
} else { } else {
debug!("payload: {:?}", event); debug!("payload: {:?}", event);
if let Some(room) = repo_config.room.as_ref().or(config.default_room.as_ref()) { if let Some(room) = repo_config.room.as_ref().or(config.default_room.as_ref()) {
if let Err(err) = event_tx.send((event, room.clone())).await { if let Err(err) = handle_gitlab_event(event, &room, &matrix_client).await {
warn!("Failed to enqueue payload: {}", err); warn!("Failed to handle payload: {}", err);
} }
warp::reply::with_status("OK", StatusCode::OK) warp::reply::with_status("OK", StatusCode::OK)
} else { } else {