treewide: remove GitHub, generalize, introduce Gerrit
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
parent
8cdae352b0
commit
584659e727
1087
Cargo.lock
generated
1087
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -12,11 +12,7 @@ chrono = "0.4.22"
|
|||
either = "1.8.0"
|
||||
fs2 = "0.4.3"
|
||||
futures-util = "0.3.25"
|
||||
#hubcaps = "0.6"
|
||||
# for Conclusion::Skipped which is in master
|
||||
hubcaps = { git = "https://github.com/softprops/hubcaps.git", rev = "d60d157b6638760fc725b2e4e4f329a4ec6b901e", default-features = false, features = ["app", "rustls-tls"] }
|
||||
# hyper = { version = "0.14", features = ["full"] }
|
||||
hyper = "=0.10.*"
|
||||
hyper = { version = "1.5.0", features = [ "client" ], package = "hyper" }
|
||||
# maybe can be removed when hyper is updated
|
||||
http = "0.2"
|
||||
lapin = "2.1.1"
|
||||
|
@ -34,3 +30,4 @@ tracing = "0.1.37"
|
|||
tracing-subscriber = { version = "0.3.16", features = ["json", "env-filter"] }
|
||||
uuid = { version = "1.2", features = ["v4"] }
|
||||
rustls-pemfile = "1.0.2"
|
||||
reqwest = "0.12.9"
|
||||
|
|
|
@ -72,31 +72,3 @@ impl CommitStatus {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommitStatusError {
|
||||
ExpiredCreds(hubcaps::Error),
|
||||
MissingSha(hubcaps::Error),
|
||||
Error(hubcaps::Error),
|
||||
}
|
||||
|
||||
impl From<hubcaps::Error> for CommitStatusError {
|
||||
fn from(e: hubcaps::Error) -> CommitStatusError {
|
||||
use http::status::StatusCode;
|
||||
use hubcaps::Error;
|
||||
match &e {
|
||||
Error::Fault { code, error }
|
||||
if code == &StatusCode::UNAUTHORIZED && error.message == "Bad credentials" =>
|
||||
{
|
||||
CommitStatusError::ExpiredCreds(e)
|
||||
}
|
||||
Error::Fault { code, error }
|
||||
if code == &StatusCode::UNPROCESSABLE_ENTITY
|
||||
&& error.message.starts_with("No commit found for SHA:") =>
|
||||
{
|
||||
CommitStatusError::MissingSha(e)
|
||||
}
|
||||
_otherwise => CommitStatusError::Error(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
use crate::acl;
|
||||
use crate::nix::Nix;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::io::Read;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use hubcaps::{Credentials, Github, InstallationTokenGenerator, JWTCredentials};
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub struct Config
|
||||
{
|
||||
pub runner: RunnerConfig,
|
||||
pub feedback: FeedbackConfig,
|
||||
pub checkout: CheckoutConfig,
|
||||
pub nix: NixConfig,
|
||||
pub rabbitmq: RabbitMqConfig,
|
||||
pub github: Option<GithubConfig>,
|
||||
pub github_app: Option<GithubAppConfig>,
|
||||
// TODO: reintroduce VCS configuration somehow
|
||||
pub log_storage: Option<LogStorage>,
|
||||
}
|
||||
|
||||
|
@ -47,17 +45,6 @@ pub struct NixConfig {
|
|||
pub initial_heap_size: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GithubConfig {
|
||||
pub token_file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GithubAppConfig {
|
||||
pub app_id: u64,
|
||||
pub private_key: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LogStorage {
|
||||
pub path: String,
|
||||
|
@ -117,25 +104,6 @@ impl Config {
|
|||
acl::Acl::new(repos, trusted_users)
|
||||
}
|
||||
|
||||
pub fn github(&self) -> Github {
|
||||
let token = std::fs::read_to_string(self.github.clone().unwrap().token_file)
|
||||
.expect("Couldn't read from GitHub token file");
|
||||
Github::new(
|
||||
"github.com/grahamc/ofborg",
|
||||
// tls configured hyper client
|
||||
Credentials::Token(token),
|
||||
)
|
||||
.expect("Unable to create a github client instance")
|
||||
}
|
||||
|
||||
pub fn github_app_vendingmachine(&self) -> GithubAppVendingMachine {
|
||||
GithubAppVendingMachine {
|
||||
conf: self.github_app.clone().unwrap(),
|
||||
id_cache: HashMap::new(),
|
||||
client_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nix(&self) -> Nix {
|
||||
if self.nix.build_timeout_seconds < 1200 {
|
||||
error!(?self.nix.build_timeout_seconds, "Please set build_timeout_seconds to at least 1200");
|
||||
|
@ -180,68 +148,6 @@ pub fn load(filename: &Path) -> Config {
|
|||
deserialized
|
||||
}
|
||||
|
||||
pub struct GithubAppVendingMachine {
|
||||
conf: GithubAppConfig,
|
||||
id_cache: HashMap<(String, String), Option<u64>>,
|
||||
client_cache: HashMap<u64, Github>,
|
||||
}
|
||||
|
||||
impl GithubAppVendingMachine {
|
||||
fn useragent(&self) -> &'static str {
|
||||
"github.com/grahamc/ofborg (app)"
|
||||
}
|
||||
|
||||
fn jwt(&self) -> JWTCredentials {
|
||||
let private_key_file =
|
||||
File::open(self.conf.private_key.clone()).expect("Unable to read private_key");
|
||||
let mut private_key_reader = BufReader::new(private_key_file);
|
||||
let private_keys = rustls_pemfile::rsa_private_keys(&mut private_key_reader)
|
||||
.expect("Unable to convert private_key to DER format");
|
||||
// We can be reasonably certain that there will only be one private key in this file
|
||||
let private_key = &private_keys[0];
|
||||
JWTCredentials::new(self.conf.app_id, private_key.to_vec())
|
||||
.expect("Unable to create JWTCredentials")
|
||||
}
|
||||
|
||||
fn install_id_for_repo(&mut self, owner: &str, repo: &str) -> Option<u64> {
|
||||
let useragent = self.useragent();
|
||||
let jwt = self.jwt();
|
||||
|
||||
let key = (owner.to_owned(), repo.to_owned());
|
||||
|
||||
*self.id_cache.entry(key).or_insert_with(|| {
|
||||
info!("Looking up install ID for {}/{}", owner, repo);
|
||||
|
||||
let lookup_gh = Github::new(useragent, Credentials::JWT(jwt)).unwrap();
|
||||
|
||||
match async_std::task::block_on(lookup_gh.app().find_repo_installation(owner, repo)) {
|
||||
Ok(install_id) => {
|
||||
debug!("Received install ID {:?}", install_id);
|
||||
Some(install_id.id)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error during install ID lookup: {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn for_repo<'a>(&'a mut self, owner: &str, repo: &str) -> Option<&'a Github> {
|
||||
let useragent = self.useragent();
|
||||
let jwt = self.jwt();
|
||||
let install_id = self.install_id_for_repo(owner, repo)?;
|
||||
|
||||
Some(self.client_cache.entry(install_id).or_insert_with(|| {
|
||||
Github::new(
|
||||
useragent,
|
||||
Credentials::InstallationToken(InstallationTokenGenerator::new(install_id, jwt)),
|
||||
)
|
||||
.expect("Unable to create a github client instance")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://stackoverflow.com/a/43627388
|
||||
fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
|
|
|
@ -41,6 +41,7 @@ pub mod systems;
|
|||
pub mod tagger;
|
||||
pub mod tasks;
|
||||
pub mod test_scratch;
|
||||
pub mod vcs;
|
||||
pub mod worker;
|
||||
pub mod writetoline;
|
||||
|
||||
|
@ -66,6 +67,7 @@ pub mod ofborg {
|
|||
pub use crate::tagger;
|
||||
pub use crate::tasks;
|
||||
pub use crate::test_scratch;
|
||||
pub use crate::vcs;
|
||||
pub use crate::worker;
|
||||
pub use crate::writetoline;
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::message::{Pr, Repo};
|
||||
|
||||
use hubcaps::checks::Conclusion;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BuildStatus {
|
||||
Skipped,
|
||||
|
@ -25,19 +23,6 @@ impl From<BuildStatus> for String {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<BuildStatus> for Conclusion {
|
||||
fn from(status: BuildStatus) -> Conclusion {
|
||||
match status {
|
||||
BuildStatus::Skipped => Conclusion::Skipped,
|
||||
BuildStatus::Success => Conclusion::Success,
|
||||
BuildStatus::Failure => Conclusion::Neutral,
|
||||
BuildStatus::HashMismatch => Conclusion::Failure,
|
||||
BuildStatus::TimedOut => Conclusion::Neutral,
|
||||
BuildStatus::UnexpectedError { .. } => Conclusion::Neutral,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LegacyBuildResult {
|
||||
pub repo: Repo,
|
||||
pub pr: Pr,
|
||||
|
|
|
@ -9,8 +9,7 @@ use crate::checkout::CachedProjectCo;
|
|||
use crate::commitstatus::{CommitStatus, CommitStatusError};
|
||||
use crate::evalchecker::EvalChecker;
|
||||
use crate::message::buildjob::BuildJob;
|
||||
|
||||
use hubcaps::checks::CheckRunOptions;
|
||||
use crate::vcs::generic::CheckRunOptions;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
40
ofborg/src/vcs/generic.rs
Normal file
40
ofborg/src/vcs/generic.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
/// Set of generic structures to abstract over a VCS in a richful way.
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CheckRunState {
|
||||
Queued,
|
||||
InProgress,
|
||||
Completed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Conclusion {
|
||||
Skipped,
|
||||
Success,
|
||||
Failure,
|
||||
Neutral,
|
||||
Cancelled,
|
||||
TimedOut,
|
||||
ActionRequired,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
pub struct CheckRunOptions {
|
||||
pub name: String,
|
||||
pub head_sha: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub details_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub external_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<CheckRunState>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conclusion: Option<Conclusion>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completed_at: Option<String>,
|
||||
}
|
318
ofborg/src/vcs/gerrit/data_structures.rs
Normal file
318
ofborg/src/vcs/gerrit/data_structures.rs
Normal file
|
@ -0,0 +1,318 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Account {
|
||||
pub name: Option<String>, // User's full name, if configured
|
||||
pub email: Option<String>, // User's preferred email address
|
||||
pub username: Option<String>, // User's username, if configured
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Approval {
|
||||
pub r#type: String, // Internal name of the approval
|
||||
pub description: String, // Human-readable category of the approval
|
||||
pub value: i32, // Value assigned by the approval (usually a numerical score)
|
||||
#[serde(rename = "oldValue")]
|
||||
pub old_value: Option<i32>, // Previous approval score, if present
|
||||
#[serde(rename = "grantedOn")]
|
||||
pub granted_on: u64, // Time in seconds since the UNIX epoch
|
||||
pub by: Account, // Reviewer of the patch set
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum ChangeType {
|
||||
Added, // The file is being created/introduced by this patch
|
||||
Modified, // The file already exists and has updated content
|
||||
Deleted, // The file existed, but is being removed by this patch
|
||||
Renamed, // The file is renamed
|
||||
Copied, // The file is copied from another file
|
||||
Rewrite, // The file was rewritten
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PatchFile {
|
||||
pub file: String, // Name of the file, or new name if renamed
|
||||
#[serde(rename = "fileOld")]
|
||||
pub file_old: Option<String>, // Old name of the file, if renamed
|
||||
pub r#type: ChangeType, // Type of change (ADDED, MODIFIED, DELETED, etc.)
|
||||
pub insertions: i64, // Number of insertions in the patch
|
||||
pub deletions: i64, // Number of deletions in the patch
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PatchSetComment {
|
||||
pub file: String, // Name of the file on which the comment was added
|
||||
pub line: u64, // Line number at which the comment was added
|
||||
pub reviewer: Account, // Account that added the comment
|
||||
pub message: String, // Comment text
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum Kind {
|
||||
Rework, // Nontrivial content changes
|
||||
TrivialRebase, // Conflict-free merge with same commit message
|
||||
TrivialRebaseWithMessageUpdate, // Conflict-free merge with message update
|
||||
MergeFirstParentUpdate, // Conflict-free change of the first (left) parent
|
||||
NoCodeChange, // No code change; same tree and parent tree
|
||||
NoChange, // No changes; same commit message and tree
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PatchSet {
|
||||
pub number: u64, // Patchset number
|
||||
pub revision: String, // Git commit for this patchset
|
||||
pub parents: Vec<String>, // List of parent revisions
|
||||
pub r#ref: String, // Git reference pointing at the revision
|
||||
pub uploader: Account, // Uploader of the patchset
|
||||
pub author: Account, // Author of the patchset
|
||||
#[serde(rename = "createdOn")]
|
||||
pub created_on: u64, // Time in seconds since the UNIX epoch
|
||||
pub kind: Kind, // Kind of change ("REWORK", "TRIVIAL_REBASE", etc.)
|
||||
pub approvals: Vec<Approval>, // Approvals granted
|
||||
pub comments: Vec<PatchSetComment>, // All comments for this patchset
|
||||
pub files: Vec<String>, // All changed files in this patchset
|
||||
#[serde(rename = "sizeInsertions")]
|
||||
pub size_insertions: i64, // Size of insertions
|
||||
#[serde(rename = "sizeDeletions")]
|
||||
pub size_deletions: i64, // Size of deletions
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ReviewerMessage {
|
||||
#[serde(rename = "message")]
|
||||
pub comment_text: String, // Comment text added by the reviewer
|
||||
pub timestamp: u64, // Time in seconds since the UNIX epoch when this comment was added
|
||||
pub reviewer: Account, // Account of the reviewer who added the comment
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct TrackingId {
|
||||
pub system: String, // Name of the system from the gerrit.config file
|
||||
pub id: String, // ID number scraped out of the commit message
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ChangeDependency {
|
||||
pub id: String, // Change identifier
|
||||
pub number: u64, // Change number
|
||||
pub revision: String, // Patchset revision
|
||||
pub r#ref: String, // Ref name
|
||||
#[serde(rename = "isCurrentPatchSet")]
|
||||
pub is_current_patch_set: bool, // If the revision is the current patchset of the change
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum ChangeStatus {
|
||||
New, // Change is still being reviewed
|
||||
Merged, // Change has been merged to its branch
|
||||
Abandoned, // Change was abandoned by its owner or administrator
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum SubmitStatus {
|
||||
Ok, // The change is ready for submission or already submitted
|
||||
NotReady, // The change is missing a required label
|
||||
RuleError, // An internal server error occurred preventing computation
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Label {
|
||||
pub label: String, // Name of the label
|
||||
pub status: LabelStatus, // Status of the label
|
||||
pub by: Account, // The account that applied the label
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum LabelStatus {
|
||||
Ok, // This label provides what is necessary for submission
|
||||
Reject, // This label prevents the change from being submitted
|
||||
Need, // The label is required for submission but has not been satisfied
|
||||
May, // The label may be set but isn’t necessary for submission
|
||||
Impossible, // The label is required but impossible to complete
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Requirement {
|
||||
#[serde(rename = "fallbackText")]
|
||||
pub fallback_text: String, // Human-readable description of the requirement
|
||||
pub r#type: String, // Alphanumerical string identifying the requirement
|
||||
pub data: Option<HashMap<String, String>>, // Additional key-value data linked to this requirement
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SubmitRecord {
|
||||
pub status: SubmitStatus, // Current submit status
|
||||
pub labels: Option<Vec<Label>>, // State of each code review label attribute
|
||||
pub requirements: Vec<Requirement>, // Requirements for submission
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Change {
|
||||
pub project: String,
|
||||
pub branch: String,
|
||||
pub topic: Option<String>,
|
||||
pub id: String,
|
||||
#[serde(rename = "number")]
|
||||
pub change_number: Option<u64>, // Deprecated, but keeping it optional
|
||||
pub subject: String,
|
||||
pub owner: Account,
|
||||
pub url: String,
|
||||
pub commit_message: String,
|
||||
pub hashtags: Vec<String>,
|
||||
#[serde(rename = "createdOn")]
|
||||
pub created_on: u64, // Time in seconds since UNIX epoch
|
||||
#[serde(rename = "lastUpdated")]
|
||||
pub last_updated: u64, // Time in seconds since UNIX epoch
|
||||
pub open: bool,
|
||||
pub status: ChangeStatus, // "NEW", "MERGED", or "ABANDONED"
|
||||
pub private: bool,
|
||||
pub wip: bool, // Work in progress
|
||||
pub comments: Vec<ReviewerMessage>, // Inline/file comments
|
||||
#[serde(rename = "trackingIds")]
|
||||
pub tracking_ids: Vec<TrackingId>, // Links to issue tracking systems
|
||||
#[serde(rename = "currentPatchSet")]
|
||||
pub current_patch_set: PatchSet,
|
||||
#[serde(rename = "patchSets")]
|
||||
pub patch_sets: Vec<PatchSet>, // All patch sets
|
||||
#[serde(rename = "dependsOn")]
|
||||
pub depends_on: Vec<ChangeDependency>, // Dependencies
|
||||
#[serde(rename = "neededBy")]
|
||||
pub needed_by: Vec<ChangeDependency>, // Reverse dependencies
|
||||
#[serde(rename = "submitRecords")]
|
||||
pub submit_records: Vec<SubmitRecord>, // Submission information
|
||||
#[serde(rename = "allReviewers")]
|
||||
pub all_reviewers: Vec<Account>, // List of all reviewers
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RefUpdate {
|
||||
#[serde(rename = "oldRev")]
|
||||
pub old_rev: String, // The old value of the ref, prior to the update
|
||||
#[serde(rename = "newRev")]
|
||||
pub new_rev: String, // The new value the ref was updated to
|
||||
pub ref_name: String, // Full ref name within the project
|
||||
pub project: String, // Project path in Gerrit
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum GerritStreamEvent {
|
||||
ChangeAbandoned {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
abandoner: Account,
|
||||
reason: String,
|
||||
event_created_on: u64
|
||||
},
|
||||
ChangeDeleted {
|
||||
change: Change,
|
||||
deleter: Account
|
||||
},
|
||||
ChangeMerged {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
submitter: Account,
|
||||
new_rev: String,
|
||||
event_created_on: u64
|
||||
},
|
||||
ChangeRestored {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
restorer: Account,
|
||||
reason: String,
|
||||
event_created_on: u64
|
||||
},
|
||||
CommentAdded {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
author: Account,
|
||||
approvals: Vec<Approval>,
|
||||
comment: String,
|
||||
event_created_on: u64
|
||||
},
|
||||
DroppedOutput,
|
||||
HashtagsChanged {
|
||||
change: Change,
|
||||
editor: Account,
|
||||
added: Vec<String>,
|
||||
removed: Vec<String>,
|
||||
hashtags: Vec<String>,
|
||||
event_created_on: u64,
|
||||
},
|
||||
ProjectCreated {
|
||||
project_name: String,
|
||||
project_head: String,
|
||||
event_created_on: u64
|
||||
},
|
||||
PatchSetCreated {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
uploader: Account,
|
||||
event_created_on: u64
|
||||
},
|
||||
RefUpdated {
|
||||
submitter: Account,
|
||||
ref_update: RefUpdate,
|
||||
event_created_on: u64
|
||||
},
|
||||
BatchRefUpdated {
|
||||
submitter: Account,
|
||||
ref_updates: Vec<RefUpdate>,
|
||||
event_created_on: u64
|
||||
},
|
||||
ReviewerAdded {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
reviewer: Account,
|
||||
adder: Account,
|
||||
event_created_on: u64
|
||||
},
|
||||
ReviewerDeleted {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
reviewer: Account,
|
||||
remover: Account,
|
||||
approvals: Vec<Approval>,
|
||||
comment: String,
|
||||
event_created_on: u64,
|
||||
},
|
||||
TopicChanged {
|
||||
change: Change,
|
||||
old_topic: Option<String>,
|
||||
new_topic: Option<String>,
|
||||
changer: Account,
|
||||
event_created_on: u64
|
||||
},
|
||||
WorkInProgressStateChanged {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
changer: Account,
|
||||
event_created_on: u64
|
||||
},
|
||||
PrivateStateChanged {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
changer: Account,
|
||||
event_created_on: u64
|
||||
},
|
||||
VoteDeleted {
|
||||
change: Change,
|
||||
patch_set: PatchSet,
|
||||
reviewer: Account,
|
||||
remover: Account,
|
||||
approvals: Vec<Approval>,
|
||||
comment: String,
|
||||
},
|
||||
ProjectHeadUpdate {
|
||||
old_head: String,
|
||||
new_head: String,
|
||||
event_created_on: u64
|
||||
}
|
||||
}
|
45
ofborg/src/vcs/gerrit/events.rs
Normal file
45
ofborg/src/vcs/gerrit/events.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use futures_util::stream::try_unfold;
|
||||
use futures_util::Stream;
|
||||
use reqwest::{Client, Response, Error};
|
||||
|
||||
use super::data_structures::GerritStreamEvent;
|
||||
|
||||
/// Streams Gerrit events back to the caller.
|
||||
async fn stream_events(gerrit_baseurl: &str) -> Result<impl Stream<Item = Result<GerritStreamEvent, Box<dyn Error>>>, Box<dyn Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
// Send the request and get a response
|
||||
let response: Response = client.get(format!("{}/stream-events", gerrit_baseurl)).send().await?;
|
||||
|
||||
// Ensure we are getting a successful response
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Failed to connect: {}", response.status()).into());
|
||||
}
|
||||
|
||||
// Create a stream from the response body
|
||||
let mut body_stream = response.bytes_stream();
|
||||
|
||||
// Create a stream of GerritStreamEvent from the body stream
|
||||
let stream = try_unfold(body_stream, |mut body_stream| async move {
|
||||
while let Some(item) = body_stream.next().await {
|
||||
let bytes = item?;
|
||||
let line = String::from_utf8_lossy(&bytes).to_string();
|
||||
|
||||
let event: Result<GerritStreamEvent, _> = serde_json::from_str(&line);
|
||||
|
||||
match event {
|
||||
Ok(event) => {
|
||||
return Ok(Some((event, body_stream)));
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Failed to deserialize event: {:?}", line);
|
||||
return Err(Box::new(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None) // End of stream
|
||||
});
|
||||
|
||||
Ok(stream)
|
||||
}
|
2
ofborg/src/vcs/gerrit/mod.rs
Normal file
2
ofborg/src/vcs/gerrit/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod data_structures;
|
||||
pub mod events;
|
141
ofborg/src/vcs/github.rs
Normal file
141
ofborg/src/vcs/github.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use async_std::io::BufReader;
|
||||
use hubcaps::{checks::Conclusion, Credentials, Github, InstallationTokenGenerator, JWTCredentials};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{config::Config, message::buildresult::BuildStatus, nix::File};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommitStatusError {
|
||||
ExpiredCreds(hubcaps::Error),
|
||||
MissingSha(hubcaps::Error),
|
||||
Error(hubcaps::Error),
|
||||
}
|
||||
|
||||
impl From<hubcaps::Error> for CommitStatusError {
|
||||
fn from(e: hubcaps::Error) -> CommitStatusError {
|
||||
use http::status::StatusCode;
|
||||
use hubcaps::Error;
|
||||
match &e {
|
||||
Error::Fault { code, error }
|
||||
if code == &StatusCode::UNAUTHORIZED && error.message == "Bad credentials" =>
|
||||
{
|
||||
CommitStatusError::ExpiredCreds(e)
|
||||
}
|
||||
Error::Fault { code, error }
|
||||
if code == &StatusCode::UNPROCESSABLE_ENTITY
|
||||
&& error.message.starts_with("No commit found for SHA:") =>
|
||||
{
|
||||
CommitStatusError::MissingSha(e)
|
||||
}
|
||||
_otherwise => CommitStatusError::Error(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BuildStatus> for Conclusion {
|
||||
fn from(status: BuildStatus) -> Conclusion {
|
||||
match status {
|
||||
BuildStatus::Skipped => Conclusion::Skipped,
|
||||
BuildStatus::Success => Conclusion::Success,
|
||||
BuildStatus::Failure => Conclusion::Neutral,
|
||||
BuildStatus::HashMismatch => Conclusion::Failure,
|
||||
BuildStatus::TimedOut => Conclusion::Neutral,
|
||||
BuildStatus::UnexpectedError { .. } => Conclusion::Neutral,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GithubConfig {
|
||||
pub token_file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GithubAppConfig {
|
||||
pub app_id: u64,
|
||||
pub private_key: PathBuf,
|
||||
}
|
||||
|
||||
pub struct GithubAppVendingMachine {
|
||||
conf: GithubAppConfig,
|
||||
id_cache: HashMap<(String, String), Option<u64>>,
|
||||
client_cache: HashMap<u64, Github>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn github(&self) -> Github {
|
||||
let token = std::fs::read_to_string(self.github.clone().unwrap().token_file)
|
||||
.expect("Couldn't read from GitHub token file");
|
||||
Github::new(
|
||||
"github.com/grahamc/ofborg",
|
||||
// tls configured hyper client
|
||||
Credentials::Token(token),
|
||||
)
|
||||
.expect("Unable to create a github client instance")
|
||||
}
|
||||
|
||||
pub fn github_app_vendingmachine(&self) -> GithubAppVendingMachine {
|
||||
GithubAppVendingMachine {
|
||||
conf: self.github_app.clone().unwrap(),
|
||||
id_cache: HashMap::new(),
|
||||
client_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GithubAppVendingMachine {
|
||||
fn useragent(&self) -> &'static str {
|
||||
"github.com/grahamc/ofborg (app)"
|
||||
}
|
||||
|
||||
fn jwt(&self) -> JWTCredentials {
|
||||
let private_key_file =
|
||||
File::open(self.conf.private_key.clone()).expect("Unable to read private_key");
|
||||
let mut private_key_reader = BufReader::new(private_key_file);
|
||||
let private_keys = rustls_pemfile::rsa_private_keys(&mut private_key_reader)
|
||||
.expect("Unable to convert private_key to DER format");
|
||||
// We can be reasonably certain that there will only be one private key in this file
|
||||
let private_key = &private_keys[0];
|
||||
JWTCredentials::new(self.conf.app_id, private_key.to_vec())
|
||||
.expect("Unable to create JWTCredentials")
|
||||
}
|
||||
|
||||
fn install_id_for_repo(&mut self, owner: &str, repo: &str) -> Option<u64> {
|
||||
let useragent = self.useragent();
|
||||
let jwt = self.jwt();
|
||||
|
||||
let key = (owner.to_owned(), repo.to_owned());
|
||||
|
||||
*self.id_cache.entry(key).or_insert_with(|| {
|
||||
info!("Looking up install ID for {}/{}", owner, repo);
|
||||
|
||||
let lookup_gh = Github::new(useragent, Credentials::JWT(jwt)).unwrap();
|
||||
|
||||
match async_std::task::block_on(lookup_gh.app().find_repo_installation(owner, repo)) {
|
||||
Ok(install_id) => {
|
||||
debug!("Received install ID {:?}", install_id);
|
||||
Some(install_id.id)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error during install ID lookup: {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn for_repo<'a>(&'a mut self, owner: &str, repo: &str) -> Option<&'a Github> {
|
||||
let useragent = self.useragent();
|
||||
let jwt = self.jwt();
|
||||
let install_id = self.install_id_for_repo(owner, repo)?;
|
||||
|
||||
Some(self.client_cache.entry(install_id).or_insert_with(|| {
|
||||
Github::new(
|
||||
useragent,
|
||||
Credentials::InstallationToken(InstallationTokenGenerator::new(install_id, jwt)),
|
||||
)
|
||||
.expect("Unable to create a github client instance")
|
||||
}))
|
||||
}
|
||||
}
|
3
ofborg/src/vcs/mod.rs
Normal file
3
ofborg/src/vcs/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod github;
|
||||
pub mod generic;
|
||||
pub mod gerrit;
|
Loading…
Reference in a new issue