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"
|
either = "1.8.0"
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
futures-util = "0.3.25"
|
futures-util = "0.3.25"
|
||||||
#hubcaps = "0.6"
|
hyper = { version = "1.5.0", features = [ "client" ], package = "hyper" }
|
||||||
# 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.*"
|
|
||||||
# maybe can be removed when hyper is updated
|
# maybe can be removed when hyper is updated
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
lapin = "2.1.1"
|
lapin = "2.1.1"
|
||||||
|
@ -34,3 +30,4 @@ tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["json", "env-filter"] }
|
tracing-subscriber = { version = "0.3.16", features = ["json", "env-filter"] }
|
||||||
uuid = { version = "1.2", features = ["v4"] }
|
uuid = { version = "1.2", features = ["v4"] }
|
||||||
rustls-pemfile = "1.0.2"
|
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::acl;
|
||||||
use crate::nix::Nix;
|
use crate::nix::Nix;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufReader, Read};
|
use std::io::Read;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use hubcaps::{Credentials, Github, InstallationTokenGenerator, JWTCredentials};
|
|
||||||
use serde::de::{self, Deserialize, Deserializer};
|
use serde::de::{self, Deserialize, Deserializer};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Config {
|
pub struct Config
|
||||||
|
{
|
||||||
pub runner: RunnerConfig,
|
pub runner: RunnerConfig,
|
||||||
pub feedback: FeedbackConfig,
|
pub feedback: FeedbackConfig,
|
||||||
pub checkout: CheckoutConfig,
|
pub checkout: CheckoutConfig,
|
||||||
pub nix: NixConfig,
|
pub nix: NixConfig,
|
||||||
pub rabbitmq: RabbitMqConfig,
|
pub rabbitmq: RabbitMqConfig,
|
||||||
pub github: Option<GithubConfig>,
|
// TODO: reintroduce VCS configuration somehow
|
||||||
pub github_app: Option<GithubAppConfig>,
|
|
||||||
pub log_storage: Option<LogStorage>,
|
pub log_storage: Option<LogStorage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,17 +45,6 @@ pub struct NixConfig {
|
||||||
pub initial_heap_size: Option<String>,
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct LogStorage {
|
pub struct LogStorage {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
@ -117,25 +104,6 @@ impl Config {
|
||||||
acl::Acl::new(repos, trusted_users)
|
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 {
|
pub fn nix(&self) -> Nix {
|
||||||
if self.nix.build_timeout_seconds < 1200 {
|
if self.nix.build_timeout_seconds < 1200 {
|
||||||
error!(?self.nix.build_timeout_seconds, "Please set build_timeout_seconds to at least 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
|
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
|
// Copied from https://stackoverflow.com/a/43627388
|
||||||
fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||||
where
|
where
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub mod systems;
|
||||||
pub mod tagger;
|
pub mod tagger;
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
pub mod test_scratch;
|
pub mod test_scratch;
|
||||||
|
pub mod vcs;
|
||||||
pub mod worker;
|
pub mod worker;
|
||||||
pub mod writetoline;
|
pub mod writetoline;
|
||||||
|
|
||||||
|
@ -66,6 +67,7 @@ pub mod ofborg {
|
||||||
pub use crate::tagger;
|
pub use crate::tagger;
|
||||||
pub use crate::tasks;
|
pub use crate::tasks;
|
||||||
pub use crate::test_scratch;
|
pub use crate::test_scratch;
|
||||||
|
pub use crate::vcs;
|
||||||
pub use crate::worker;
|
pub use crate::worker;
|
||||||
pub use crate::writetoline;
|
pub use crate::writetoline;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
use crate::message::{Pr, Repo};
|
use crate::message::{Pr, Repo};
|
||||||
|
|
||||||
use hubcaps::checks::Conclusion;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum BuildStatus {
|
pub enum BuildStatus {
|
||||||
Skipped,
|
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 struct LegacyBuildResult {
|
||||||
pub repo: Repo,
|
pub repo: Repo,
|
||||||
pub pr: Pr,
|
pub pr: Pr,
|
||||||
|
|
|
@ -9,8 +9,7 @@ use crate::checkout::CachedProjectCo;
|
||||||
use crate::commitstatus::{CommitStatus, CommitStatusError};
|
use crate::commitstatus::{CommitStatus, CommitStatusError};
|
||||||
use crate::evalchecker::EvalChecker;
|
use crate::evalchecker::EvalChecker;
|
||||||
use crate::message::buildjob::BuildJob;
|
use crate::message::buildjob::BuildJob;
|
||||||
|
use crate::vcs::generic::CheckRunOptions;
|
||||||
use hubcaps::checks::CheckRunOptions;
|
|
||||||
|
|
||||||
use std::path::Path;
|
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