diff --git a/dco2-aws-lambda/src/main.rs b/dco2-aws-lambda/src/main.rs index 16e3386..27acaea 100644 --- a/dco2-aws-lambda/src/main.rs +++ b/dco2-aws-lambda/src/main.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use dco2::github::{self, GHClientOctorust}; +use dco2::github::client::{AppConfig, GHClientOctorust}; use dco2_server::handlers::setup_router; use figment::{providers::Env, Figment}; use lambda_http::{run, tracing, Error}; @@ -15,7 +15,7 @@ async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); // Setup GitHub client - let cfg: github::AppConfig = Figment::new() + let cfg: AppConfig = Figment::new() .merge(Env::prefixed("DCO2_")) .extract() .context("error setting up configuration")?; diff --git a/dco2-server/src/config.rs b/dco2-server/src/config.rs index c480f18..ef921ca 100644 --- a/dco2-server/src/config.rs +++ b/dco2-server/src/config.rs @@ -1,7 +1,7 @@ //! This module defines some types to represent the server configuration. use anyhow::Result; -use dco2::github::AppConfig; +use dco2::github::client::AppConfig; use figment::{ providers::{Env, Format, Serialized, Yaml}, Figment, diff --git a/dco2-server/src/handlers.rs b/dco2-server/src/handlers.rs index 272b2f5..a3f6ae1 100644 --- a/dco2-server/src/handlers.rs +++ b/dco2-server/src/handlers.rs @@ -11,7 +11,10 @@ use axum::{ }; use dco2::{ dco, - github::{DynGHClient, Event, EventError, EVENT_ID_HEADER, SIGNATURE_HEADER}, + github::{ + client::DynGHClient, + event::{Event, EventError, EVENT_ID_HEADER, EVENT_SIGNATURE_HEADER}, + }, }; use hmac::{Hmac, Mac}; use sha2::Sha256; @@ -91,7 +94,7 @@ async fn event( #[allow(clippy::missing_errors_doc)] pub fn verify_signature(secret: &[u8], headers: &HeaderMap, body: &[u8]) -> Result<()> { if let Some(signature) = headers - .get(SIGNATURE_HEADER) + .get(EVENT_SIGNATURE_HEADER) .and_then(|s| s.to_str().ok()) .and_then(|s| s.strip_prefix("sha256=")) .and_then(|s| hex::decode(s).ok()) diff --git a/dco2-server/src/main.rs b/dco2-server/src/main.rs index 80cfd61..6e782cb 100644 --- a/dco2-server/src/main.rs +++ b/dco2-server/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use clap::Parser; use config::{Config, LogFormat}; -use dco2::github::GHClientOctorust; +use dco2::github::client::GHClientOctorust; use dco2_server::handlers::setup_router; use std::{path::PathBuf, sync::Arc}; use tokio::{net::TcpListener, signal}; diff --git a/dco2/src/dco/mod.rs b/dco2/src/dco/mod.rs index ce648f5..2266933 100644 --- a/dco2/src/dco/mod.rs +++ b/dco2/src/dco/mod.rs @@ -3,8 +3,8 @@ use crate::{ dco, github::{ - CheckRun, CheckRunAction, CheckRunConclusion, CheckRunEvent, CheckRunEventAction, CheckRunStatus, - Commit, DynGHClient, Event, PullRequestEvent, PullRequestEventAction, + client::{CheckRun, CheckRunAction, CheckRunConclusion, CheckRunStatus, Commit, DynGHClient}, + event::{CheckRunEvent, CheckRunEventAction, Event, PullRequestEvent, PullRequestEventAction}, }, }; use anyhow::{Context, Result}; @@ -159,7 +159,7 @@ pub async fn process_pull_request_event(gh_client: DynGHClient, event: &PullRequ CheckRunConclusion::ActionRequired, vec![CheckRunAction { label: "Set DCO to pass".to_string(), - description: "Override check result setting it to passed".to_string(), + description: "Manually set DCO check result to passed".to_string(), identifier: ACTION_OVERRIDE.to_string(), }], ) diff --git a/dco2/src/dco/tests.rs b/dco2/src/dco/tests.rs index f8fa771..f6ce105 100644 --- a/dco2/src/dco/tests.rs +++ b/dco2/src/dco/tests.rs @@ -1,7 +1,6 @@ -use super::{check, CheckInput}; use crate::{ - dco::{CheckOutput, CommitCheckOutput, CommitError}, - github::{Commit, GitUser}, + dco::{check, CheckInput, CheckOutput, CommitCheckOutput, CommitError}, + github::client::{Commit, GitUser}, }; use indoc::indoc; use std::vec; diff --git a/dco2/src/github.rs b/dco2/src/github/client.rs similarity index 60% rename from dco2/src/github.rs rename to dco2/src/github/client.rs index 8e45cad..85c7735 100644 --- a/dco2/src/github.rs +++ b/dco2/src/github/client.rs @@ -2,21 +2,9 @@ use anyhow::Result; use async_trait::async_trait; -use bytes::Bytes; use chrono::{DateTime, Utc}; -use http::HeaderMap; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use thiserror::Error; - -/// Header representing the event unique identifier. -pub const EVENT_ID_HEADER: &str = "X-GitHub-Delivery"; - -/// Header representing the name of the event received. -pub const EVENT_NAME_HEADER: &str = "X-GitHub-Event"; - -/// Header representing the event payload signature. -pub const SIGNATURE_HEADER: &str = "X-Hub-Signature-256"; /// Abstraction layer over a GitHub client. This trait defines the methods that /// a GHClient implementation must provide. @@ -179,42 +167,6 @@ impl From for octorust::types::ChecksCreateRequestConclusion } } -/// Check run event payload. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CheckRunEvent { - pub action: CheckRunEventAction, - pub check_run: CheckRunEventCheckRun, - pub installation: Installation, - pub repository: Repository, - pub requested_action: RequestedAction, -} - -impl CheckRunEvent { - /// Get context information from event details. - pub fn ctx(&self) -> Ctx { - let (owner, repo) = split_full_name(&self.repository.full_name); - Ctx { - inst_id: self.installation.id, - owner: owner.to_string(), - repo: repo.to_string(), - } - } -} - -/// Check run event action. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CheckRunEventAction { - RequestedAction, - Rerequested, -} - -/// Check run event check run details. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CheckRunEventCheckRun { - pub head_sha: String, -} - /// Check run status. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -231,6 +183,14 @@ impl From for octorust::types::JobStatus { } } +/// Information about the target of a GitHub API request. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Ctx { + pub inst_id: i64, + pub owner: String, + pub repo: String, +} + /// Commit information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Commit { @@ -264,54 +224,6 @@ impl From for Commit { } } -/// Information about the target of a GitHub API request. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Ctx { - pub inst_id: i64, - pub owner: String, - pub repo: String, -} - -/// Webhook event. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Event { - CheckRun(CheckRunEvent), - PullRequest(PullRequestEvent), -} - -impl TryFrom<(&HeaderMap, &Bytes)> for Event { - type Error = EventError; - - /// Try to create a new event instance from the provided headers and body. - fn try_from((headers, body): (&HeaderMap, &Bytes)) -> Result { - match headers.get(EVENT_NAME_HEADER) { - Some(event_name) => match event_name.as_bytes() { - b"check_run" => { - let event = serde_json::from_slice(body).map_err(|_| EventError::InvalidPayload)?; - Ok(Event::CheckRun(event)) - } - b"pull_request" => { - let event = serde_json::from_slice(body).map_err(|_| EventError::InvalidPayload)?; - Ok(Event::PullRequest(event)) - } - _ => Err(EventError::UnsupportedEvent), - }, - None => Err(EventError::MissingHeader), - } - } -} - -/// Errors that may occur while creating a new event instance. -#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum EventError { - #[error("invalid payload")] - InvalidPayload, - #[error("event header missing")] - MissingHeader, - #[error("unsupported event")] - UnsupportedEvent, -} - /// Git user information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct GitUser { @@ -319,83 +231,3 @@ pub struct GitUser { pub email: String, pub is_bot: bool, } - -/// GitHub application installation information. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Installation { - pub id: i64, -} - -/// Pull request information. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct PullRequest { - pub base: PullRequestBase, - pub head: PullRequestHead, - pub html_url: String, -} - -/// Pull request base information. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct PullRequestBase { - #[serde(rename = "ref")] - pub ref_: String, - pub sha: String, -} - -/// Pull request event payload. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct PullRequestEvent { - pub action: PullRequestEventAction, - pub installation: Installation, - pub pull_request: PullRequest, - pub repository: Repository, -} - -impl PullRequestEvent { - /// Get context information from event details. - pub fn ctx(&self) -> Ctx { - let (owner, repo) = split_full_name(&self.repository.full_name); - Ctx { - inst_id: self.installation.id, - owner: owner.to_string(), - repo: repo.to_string(), - } - } -} - -/// Pull request event action. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum PullRequestEventAction { - Opened, - Synchronize, - #[serde(other)] - Other, -} - -/// Pull request head information. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct PullRequestHead { - #[serde(rename = "ref")] - pub ref_: String, - pub sha: String, -} - -/// Repository information. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Repository { - pub full_name: String, -} - -/// Requested action information. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RequestedAction { - pub identifier: String, -} - -/// Helper function that splits a repository's full name and returns the owner -/// and the repo name as a tuple. -fn split_full_name(full_name: &str) -> (&str, &str) { - let mut parts = full_name.split('/'); - (parts.next().unwrap(), parts.next().unwrap()) -} diff --git a/dco2/src/github/event.rs b/dco2/src/github/event.rs new file mode 100644 index 0000000..6bbaf3c --- /dev/null +++ b/dco2/src/github/event.rs @@ -0,0 +1,173 @@ +//! This module defines some types and functions to parse and deserialize +//! GitHub webhook events. + +use super::client::Ctx; +use bytes::Bytes; +use http::HeaderMap; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Header representing the event unique identifier. +pub const EVENT_ID_HEADER: &str = "X-GitHub-Delivery"; + +/// Header representing the name of the event received. +pub const EVENT_NAME_HEADER: &str = "X-GitHub-Event"; + +/// Header representing the event payload signature. +pub const EVENT_SIGNATURE_HEADER: &str = "X-Hub-Signature-256"; + +/// Webhook event. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Event { + CheckRun(CheckRunEvent), + PullRequest(PullRequestEvent), +} + +impl TryFrom<(&HeaderMap, &Bytes)> for Event { + type Error = EventError; + + /// Try to create a new event instance from the provided headers and body. + fn try_from((headers, body): (&HeaderMap, &Bytes)) -> Result { + match headers.get(EVENT_NAME_HEADER) { + Some(event_name) => match event_name.as_bytes() { + b"check_run" => { + let event = serde_json::from_slice(body).map_err(|_| EventError::InvalidPayload)?; + Ok(Event::CheckRun(event)) + } + b"pull_request" => { + let event = serde_json::from_slice(body).map_err(|_| EventError::InvalidPayload)?; + Ok(Event::PullRequest(event)) + } + _ => Err(EventError::UnsupportedEvent), + }, + None => Err(EventError::MissingHeader), + } + } +} + +/// Errors that may occur while creating a new event instance. +#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum EventError { + #[error("invalid payload")] + InvalidPayload, + #[error("event header missing")] + MissingHeader, + #[error("unsupported event")] + UnsupportedEvent, +} + +/// Check run event payload. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CheckRunEvent { + pub action: CheckRunEventAction, + pub check_run: CheckRunEventCheckRun, + pub installation: Installation, + pub repository: Repository, + pub requested_action: RequestedAction, +} + +impl CheckRunEvent { + /// Get context information from event details. + pub fn ctx(&self) -> Ctx { + let (owner, repo) = split_full_name(&self.repository.full_name); + Ctx { + inst_id: self.installation.id, + owner: owner.to_string(), + repo: repo.to_string(), + } + } +} + +/// Check run event action. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CheckRunEventAction { + RequestedAction, + Rerequested, +} + +/// Check run event check run details. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CheckRunEventCheckRun { + pub head_sha: String, +} + +/// GitHub application installation information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Installation { + pub id: i64, +} + +/// Pull request information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PullRequest { + pub base: PullRequestBase, + pub head: PullRequestHead, + pub html_url: String, +} + +/// Pull request base information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PullRequestBase { + #[serde(rename = "ref")] + pub ref_: String, + pub sha: String, +} + +/// Pull request event payload. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PullRequestEvent { + pub action: PullRequestEventAction, + pub installation: Installation, + pub pull_request: PullRequest, + pub repository: Repository, +} + +impl PullRequestEvent { + /// Get context information from event details. + pub fn ctx(&self) -> Ctx { + let (owner, repo) = split_full_name(&self.repository.full_name); + Ctx { + inst_id: self.installation.id, + owner: owner.to_string(), + repo: repo.to_string(), + } + } +} + +/// Pull request event action. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PullRequestEventAction { + Opened, + Synchronize, + #[serde(other)] + Other, +} + +/// Pull request head information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PullRequestHead { + #[serde(rename = "ref")] + pub ref_: String, + pub sha: String, +} + +/// Repository information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Repository { + pub full_name: String, +} + +/// Requested action information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RequestedAction { + pub identifier: String, +} + +/// Helper function that splits a repository's full name and returns the owner +/// and the repo name as a tuple. +fn split_full_name(full_name: &str) -> (&str, &str) { + let mut parts = full_name.split('/'); + (parts.next().unwrap(), parts.next().unwrap()) +} diff --git a/dco2/src/github/mod.rs b/dco2/src/github/mod.rs new file mode 100644 index 0000000..2ab0eb6 --- /dev/null +++ b/dco2/src/github/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod event;