WIP: generalize for Gerrit / Floral #3

Draft
raito wants to merge 33 commits from vcs-generalization into main
12 changed files with 21 additions and 1884 deletions
Showing only changes of commit 96d583f34e - Show all commits

482
Cargo.lock generated
View file

@ -386,18 +386,6 @@ dependencies = [
"safemem",
]
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.7"
@ -479,9 +467,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.1.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cbc"
@ -681,7 +669,7 @@ dependencies = [
"asn1-rs",
"displaydoc",
"nom 7.1.3",
"num-bigint 0.4.6",
"num-bigint",
"num-traits",
"rusticata-macros",
]
@ -770,21 +758,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
@ -860,7 +833,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin 0.9.8",
"spin",
]
[[package]]
@ -1033,31 +1006,6 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "h2"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]]
name = "heck"
version = "0.5.0"
@ -1102,50 +1050,12 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hubcaps"
version = "0.6.2"
source = "git+https://github.com/softprops/hubcaps.git?rev=d60d157b6638760fc725b2e4e4f329a4ec6b901e#d60d157b6638760fc725b2e4e4f329a4ec6b901e"
dependencies = [
"base64 0.13.1",
"data-encoding",
"futures",
"http",
"hyperx",
"jsonwebtoken",
"log 0.4.22",
"mime 0.3.17",
"percent-encoding 2.1.0",
"reqwest",
"serde",
"serde_derive",
"serde_json",
"url 2.3.0",
]
[[package]]
name = "hyper"
version = "0.10.16"
@ -1154,71 +1064,17 @@ checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273"
dependencies = [
"base64 0.9.3",
"httparse",
"language-tags 0.2.2",
"language-tags",
"log 0.3.9",
"mime 0.2.6",
"mime",
"num_cpus",
"time 0.1.45",
"traitobject",
"typeable",
"unicase 1.4.2",
"unicase",
"url 1.7.2",
]
[[package]]
name = "hyper"
version = "0.14.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http",
"hyper 0.14.31",
"rustls 0.21.12",
"tokio",
"tokio-rustls",
]
[[package]]
name = "hyperx"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5617e92fc2f2501c3e2bc6ce547cad841adba2bae5b921c7e52510beca6d084c"
dependencies = [
"base64 0.13.1",
"bytes",
"http",
"httpdate",
"language-tags 0.3.2",
"mime 0.3.17",
"percent-encoding 2.1.0",
"unicase 2.6.0",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -1264,16 +1120,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.3"
@ -1304,12 +1150,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "ipnet"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1354,32 +1194,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32"
dependencies = [
"base64 0.12.3",
"pem",
"ring 0.16.20",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "language-tags"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
[[package]]
name = "language-tags"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "lapin"
version = "2.5.0"
@ -1512,12 +1332,6 @@ dependencies = [
"log 0.3.9",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1575,17 +1389,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@ -1653,8 +1456,7 @@ dependencies = [
"futures",
"futures-util",
"http",
"hubcaps",
"hyper 0.10.16",
"hyper",
"jfs",
"lapin",
"lru-cache",
@ -1795,17 +1597,6 @@ dependencies = [
"hmac",
]
[[package]]
name = "pem"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
dependencies = [
"base64 0.13.1",
"once_cell",
"regex",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@ -2076,62 +1867,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper 0.14.31",
"hyper-rustls",
"ipnet",
"js-sys",
"log 0.4.22",
"mime 0.3.17",
"once_cell",
"percent-encoding 2.1.0",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-rustls",
"tower-service",
"url 2.3.0",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"winreg",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin 0.5.2",
"untrusted 0.7.1",
"web-sys",
"winapi",
]
[[package]]
name = "ring"
version = "0.17.8"
@ -2142,8 +1877,8 @@ dependencies = [
"cfg-if",
"getrandom",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
@ -2189,18 +1924,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log 0.4.22",
"ring 0.17.8",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.16"
@ -2208,9 +1931,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
dependencies = [
"once_cell",
"ring 0.17.8",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"rustls-webpki",
"subtle",
"zeroize",
]
@ -2222,10 +1945,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a980454b497c439c274f2feae2523ed8138bbd3d323684e1435fec62f800481"
dependencies = [
"log 0.4.22",
"rustls 0.23.16",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"rustls-webpki",
]
[[package]]
@ -2265,25 +1988,15 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring 0.17.8",
"untrusted 0.9.0",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring 0.17.8",
"ring",
"rustls-pki-types",
"untrusted 0.9.0",
"untrusted",
]
[[package]]
@ -2333,16 +2046,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring 0.17.8",
"untrusted 0.9.0",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -2404,18 +2107,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -2477,17 +2168,6 @@ dependencies = [
"libc",
]
[[package]]
name = "simple_asn1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
dependencies = [
"chrono",
"num-bigint 0.2.6",
"num-traits",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -2523,12 +2203,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.8"
@ -2571,12 +2245,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.13.1"
@ -2598,27 +2266,6 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tcp-stream"
version = "0.28.0"
@ -2779,16 +2426,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
@ -2800,25 +2437,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.40"
@ -2899,12 +2517,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typeable"
version = "0.1.2"
@ -2926,15 +2538,6 @@ dependencies = [
"version_check 0.1.5",
]
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check 0.9.5",
]
[[package]]
name = "unicode-bidi"
version = "0.3.17"
@ -2956,12 +2559,6 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -3029,15 +2626,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -3076,18 +2664,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
@ -3117,22 +2693,6 @@ version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "web-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "winapi"
version = "0.3.9"
@ -3312,16 +2872,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "x509-cert"
version = "0.2.5"

View file

@ -14,12 +14,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.*"
# maybe can be removed when hyper is updated
http = "0.2"
lapin = "2.5.0"
lru-cache = "0.1.2"

View file

@ -2,7 +2,6 @@ use std::env;
use std::error::Error;
use std::path::Path;
use std::process;
use std::sync::RwLock;
use futures::executor::block_on;
use ofborg::config::VCSConfig;
@ -55,7 +54,6 @@ fn main() -> Result<(), Box<dyn Error>> {
})?;
let vcs_data = match cfg.vcs {
VCSConfig::GitHub => SupportedVCS::GitHub(RwLock::new(cfg.github_app_vendingmachine())),
VCSConfig::Gerrit => SupportedVCS::Gerrit,
};

View file

@ -1,21 +1,17 @@
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 futures::executor::block_on;
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 enum VCSConfig {
GitHub,
Gerrit,
}
@ -31,10 +27,6 @@ pub struct Config {
pub pastebin: PastebinConfig,
pub log_storage: Option<LogStorage>,
// GitHub-specific configuration if vcs == GitHub.
pub github: Option<GithubConfig>,
pub github_app: Option<GithubAppConfig>,
// Gerrit-specific configuration if vcs == Gerrit.
pub gerrit: Option<GerritConfig>,
}
@ -92,17 +84,6 @@ pub struct GerritConfig {
pub ssh_port: u16,
}
#[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,
@ -163,25 +144,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");
@ -234,68 +196,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 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

View file

@ -1,7 +1,6 @@
/// This is what evaluates every pull-request
use crate::acl::Acl;
use crate::checkout;
use crate::config::GithubAppVendingMachine;
use crate::files::file_to_str;
use crate::message::{buildjob, evaluationjob};
use crate::nix;
@ -12,19 +11,16 @@ use crate::utils::pastebin::PersistedPastebin;
use crate::vcs::commit_status::{CommitStatus, CommitStatusError};
use crate::vcs::generic::{Issue, IssueState, State, VersionControlSystemAPI};
use crate::vcs::gerrit::http::GerritHTTPApi;
use crate::vcs::github::compat::GitHubAPI;
use crate::worker;
use std::path::Path;
use std::rc::Rc;
use std::sync::RwLock;
use std::time::Instant;
use futures::executor::block_on;
use tracing::{debug_span, error, info, warn};
pub enum SupportedVCS {
GitHub(RwLock<GithubAppVendingMachine>),
Gerrit,
}
@ -89,15 +85,6 @@ impl<E: stats::SysEvents + 'static> worker::SimpleWorker for EvaluationWorker<E>
let _enter = span.enter();
let vcs_api: Rc<dyn VersionControlSystemAPI> = match self.vcs {
SupportedVCS::GitHub(ref vending_machine) => {
let mut vending_machine = vending_machine
.write()
.expect("Failed to get write lock on github vending machine");
let github_client = vending_machine
.for_repo(&job.repo.owner, &job.repo.name)
.expect("Failed to get a github client token");
Rc::new(GitHubAPI::new(github_client.clone()))
}
SupportedVCS::Gerrit => Rc::new(GerritHTTPApi),
};

View file

@ -1,168 +0,0 @@
use crate::acl;
use crate::commentparser;
use crate::ghevent;
use crate::message::{buildjob, evaluationjob, Change, Repo};
use crate::worker;
use futures::executor::block_on;
use tracing::{debug_span, error, info};
use uuid::Uuid;
pub struct GitHubCommentWorker {
acl: acl::Acl,
github: hubcaps::Github,
}
impl GitHubCommentWorker {
pub fn new(acl: acl::Acl, github: hubcaps::Github) -> GitHubCommentWorker {
GitHubCommentWorker { acl, github }
}
}
impl worker::SimpleWorker for GitHubCommentWorker {
type J = ghevent::IssueComment;
fn msg_to_job(&mut self, _: &str, _: &Option<String>, body: &[u8]) -> Result<Self::J, String> {
match serde_json::from_slice(body) {
Ok(e) => Ok(e),
Err(e) => {
error!(
"Failed to deserialize IsssueComment: {:?}",
String::from_utf8(body.to_vec())
);
panic!("{:?}", e);
}
}
}
// FIXME: remove with rust/cargo update
#[allow(clippy::cognitive_complexity)]
fn consumer(
&mut self,
_chan: &mut lapin::Channel,
job: &ghevent::IssueComment,
) -> worker::Actions {
let span = debug_span!("job", pr = ?job.issue.number);
let _enter = span.enter();
if job.action == ghevent::IssueCommentAction::Deleted {
return vec![worker::Action::Ack];
}
let instructions = commentparser::parse(&job.comment.body);
if instructions.is_none() {
return vec![worker::Action::Ack];
}
let build_destinations = self.acl.build_job_architectures_for_user_repo(
&job.comment.user.login,
&job.repository.full_name,
);
if build_destinations.is_empty() {
info!("No build destinations for: {:?}", job);
// Don't process comments if they can't build anything
return vec![worker::Action::Ack];
}
info!("Got job: {:?}", job);
let instructions = commentparser::parse(&job.comment.body);
info!("Instructions: {:?}", instructions);
let pr = block_on(
self.github
.repo(
job.repository.owner.login.clone(),
job.repository.name.clone(),
)
.pulls()
.get(job.issue.number)
.get(),
);
if let Err(x) = pr {
info!(
"fetching PR {}#{} from GitHub yielded error {}",
job.repository.full_name, job.issue.number, x
);
return vec![worker::Action::Ack];
}
let pr = pr.unwrap();
let repo_msg = Repo {
clone_url: job.repository.clone_url.clone(),
full_name: job.repository.full_name.clone(),
owner: job.repository.owner.login.clone(),
name: job.repository.name.clone(),
};
let pr_msg = Change {
number: job.issue.number,
head_sha: pr.head.sha.clone(),
target_branch: Some(pr.base.commit_ref),
};
let mut response: Vec<worker::Action> = vec![];
if let Some(instructions) = instructions {
for instruction in instructions {
match instruction {
commentparser::Instruction::Build(subset, attrs) => {
let build_destinations = match subset {
commentparser::Subset::NixOS => build_destinations
.clone()
.into_iter()
.filter(|x| x.can_run_nixos_tests())
.collect(),
_ => build_destinations.clone(),
};
let msg = buildjob::BuildJob::new(
repo_msg.clone(),
pr_msg.clone(),
subset,
attrs,
None,
None,
format!("{}", Uuid::new_v4()),
);
for arch in build_destinations.iter() {
let (exchange, routingkey) = arch.as_build_destination();
response.push(worker::publish_serde_action(exchange, routingkey, &msg));
}
response.push(worker::publish_serde_action(
Some("build-results".to_string()),
None,
&buildjob::QueuedBuildJobs {
job: msg,
architectures: build_destinations
.iter()
.cloned()
.map(|arch| arch.to_string())
.collect(),
},
));
}
commentparser::Instruction::Eval => {
let msg = evaluationjob::EvaluationJob {
repo: repo_msg.clone(),
change: pr_msg.clone(),
};
response.push(worker::publish_serde_action(
None,
Some("mass-rebuild-check-jobs".to_owned()),
&msg,
));
}
}
}
}
response.push(worker::Action::Ack);
response
}
}

View file

@ -1,760 +0,0 @@
use crate::config::GithubAppVendingMachine;
use crate::message::buildjob::{BuildJob, QueuedBuildJobs};
use crate::message::buildresult::{BuildResult, BuildStatus, LegacyBuildResult};
use crate::message::Repo;
use crate::worker;
use chrono::{DateTime, Utc};
use futures::executor::block_on;
use hubcaps::checks::{CheckRunOptions, CheckRunState, Conclusion, Output};
use tracing::{debug, debug_span, info};
pub struct GitHubCommentPoster {
github_vend: GithubAppVendingMachine,
}
impl GitHubCommentPoster {
pub fn new(github_vend: GithubAppVendingMachine) -> GitHubCommentPoster {
GitHubCommentPoster { github_vend }
}
}
pub enum PostableEvent {
BuildQueued(QueuedBuildJobs),
BuildFinished(BuildResult),
}
impl PostableEvent {
fn from(bytes: &[u8]) -> Result<PostableEvent, String> {
match serde_json::from_slice::<QueuedBuildJobs>(bytes) {
Ok(e) => Ok(PostableEvent::BuildQueued(e)),
Err(_) => match serde_json::from_slice::<BuildResult>(bytes) {
Ok(e) => Ok(PostableEvent::BuildFinished(e)),
Err(e) => Err(format!(
"Failed to deserialize PostableEvent: {:?}, err: {:}",
String::from_utf8_lossy(bytes),
e
)),
},
}
}
}
impl worker::SimpleWorker for GitHubCommentPoster {
type J = PostableEvent;
fn msg_to_job(&mut self, _: &str, _: &Option<String>, body: &[u8]) -> Result<Self::J, String> {
PostableEvent::from(body)
}
fn consumer(&mut self, _chan: &mut lapin::Channel, job: &PostableEvent) -> worker::Actions {
let mut checks: Vec<CheckRunOptions> = vec![];
let repo: Repo;
let pr = match job {
PostableEvent::BuildQueued(queued_job) => {
repo = queued_job.job.repo.clone();
for architecture in queued_job.architectures.iter() {
checks.push(job_to_check(&queued_job.job, architecture, Utc::now()));
}
queued_job.job.change.to_owned()
}
PostableEvent::BuildFinished(finished_job) => {
let result = finished_job.legacy();
repo = result.repo.clone();
checks.push(result_to_check(&result, Utc::now()));
finished_job.change()
}
};
let span = debug_span!("job", pr = ?pr.number);
let _enter = span.enter();
for check in checks {
info!(
"check {:?} {} {}",
check.status,
check.name,
check.details_url.as_ref().unwrap_or(&String::from("-"))
);
debug!("{:?}", check);
let check_create_attempt = block_on(
self.github_vend
.for_repo(&repo.owner, &repo.name)
.unwrap()
.repo(repo.owner.clone(), repo.name.clone())
.checkruns()
.create(&check),
);
match check_create_attempt {
Ok(_) => info!("Successfully sent."),
Err(err) => info!("Failed to send check {:?}", err),
}
}
vec![worker::Action::Ack]
}
}
fn job_to_check(job: &BuildJob, architecture: &str, timestamp: DateTime<Utc>) -> CheckRunOptions {
let mut all_attrs: Vec<String> = job.attrs.clone();
all_attrs.sort();
if all_attrs.is_empty() {
all_attrs = vec![String::from("(unknown attributes)")];
}
CheckRunOptions {
name: format!("{} on {}", all_attrs.join(", "), architecture),
actions: None,
completed_at: None,
started_at: Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
conclusion: None,
details_url: Some(format!(
"https://logs.ofborg.org/?key={}/{}.{}",
&job.repo.owner.to_lowercase(),
&job.repo.name.to_lowercase(),
job.change.number,
)),
external_id: None,
head_sha: job.change.head_sha.clone(),
output: None,
status: Some(CheckRunState::Queued),
}
}
fn result_to_check(result: &LegacyBuildResult, timestamp: DateTime<Utc>) -> CheckRunOptions {
let mut all_attrs: Vec<String> =
vec![result.attempted_attrs.clone(), result.skipped_attrs.clone()]
.into_iter()
.map(|opt| opt.unwrap_or_else(|| vec![]))
.flat_map(|list| list.into_iter())
.collect();
all_attrs.sort();
if all_attrs.is_empty() {
all_attrs = vec![String::from("(unknown attributes)")];
}
let conclusion: Conclusion = result.status.clone().into();
let mut summary: Vec<String> = vec![];
if let Some(ref attempted) = result.attempted_attrs {
summary.extend(list_segment("Attempted", attempted));
}
if result.status == BuildStatus::TimedOut {
summary.push(String::from("Build timed out."));
}
if let Some(ref skipped) = result.skipped_attrs {
summary.extend(list_segment(
&format!(
"The following builds were skipped because they don't evaluate on {}",
result.system
),
skipped,
));
}
// Allow the clippy violation for improved readability
#[allow(clippy::vec_init_then_push)]
let text: String = if !result.output.is_empty() {
let mut reply: Vec<String> = vec![];
reply.push("## Partial log".to_owned());
reply.push("".to_owned());
reply.push("```".to_owned());
reply.extend(result.output.clone());
reply.push("```".to_owned());
reply.join("\n")
} else {
String::from("No partial log is available.")
};
CheckRunOptions {
name: format!("{} on {}", all_attrs.join(", "), result.system),
actions: None,
completed_at: Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
started_at: None,
conclusion: Some(conclusion),
details_url: Some(format!(
"https://logs.ofborg.org/?key={}/{}.{}&attempt_id={}",
&result.repo.owner.to_lowercase(),
&result.repo.name.to_lowercase(),
result.pr.number,
result.attempt_id,
)),
external_id: Some(result.attempt_id.clone()),
head_sha: result.pr.head_sha.clone(),
output: Some(Output {
annotations: None,
images: None,
summary: summary.join("\n"),
text: Some(text),
title: result.status.clone().into(),
}),
status: Some(CheckRunState::Completed),
}
}
fn list_segment(name: &str, things: &[String]) -> Vec<String> {
let mut reply: Vec<String> = vec![];
if !things.is_empty() {
reply.push(format!("{}: {}", name, things.join(", ")));
reply.push("".to_owned());
}
reply
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::{Change, Repo};
use chrono::TimeZone;
#[test]
pub fn test_queued_build() {
let job = BuildJob {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
change: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
logs: None,
statusreport: None,
subset: None,
request_id: "bogus-request-id".to_owned(),
attrs: vec!["foo".to_owned(), "bar".to_owned()],
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
job_to_check(&job, "x86_64-linux", timestamp),
CheckRunOptions {
name: "bar, foo on x86_64-linux".to_string(),
actions: None,
started_at: Some("2023-04-20T13:37:42Z".to_string()),
completed_at: None,
status: Some(CheckRunState::Queued),
conclusion: None,
details_url: Some("https://logs.ofborg.org/?key=nixos/nixpkgs.2345".to_string()),
external_id: None,
head_sha: "abc123".to_string(),
output: None,
}
);
}
#[test]
pub fn test_check_passing_build() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec![
"make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[2]: Nothing to be done for 'install'.".to_owned(),
"make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[1]: Nothing to be done for 'install-target'.".to_owned(),
"make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'".to_owned(),
"removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'".to_owned(),
"post-installation fixup".to_owned(),
"strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip".to_owned(),
"patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
"/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: Some(vec!["bar".to_owned()]),
status: BuildStatus::Success,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "bar, foo on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Success),
details_url: Some(
"https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "Success".to_string(),
summary: "Attempted: foo
The following builds were skipped because they don't evaluate on x86_64-linux: bar
"
.to_string(),
text: Some(
"## Partial log
```
make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[2]: Nothing to be done for 'install'.
make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[1]: Nothing to be done for 'install-target'.
make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'
removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'
post-installation fixup
strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip
patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
```"
.to_string()
),
annotations: None,
images: None,
})
}
);
}
#[test]
pub fn test_check_failing_build() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec![
"make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[2]: Nothing to be done for 'install'.".to_owned(),
"make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[1]: Nothing to be done for 'install-target'.".to_owned(),
"make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'".to_owned(),
"removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'".to_owned(),
"post-installation fixup".to_owned(),
"strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip".to_owned(),
"patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
"/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: None,
status: BuildStatus::Failure,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "foo on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
"https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "Failure".to_string(),
summary: "Attempted: foo
"
.to_string(),
text: Some(
"## Partial log
```
make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[2]: Nothing to be done for 'install'.
make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[1]: Nothing to be done for 'install-target'.
make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'
removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'
post-installation fixup
strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip
patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
```"
.to_string()
),
annotations: None,
images: None,
})
}
);
}
#[test]
pub fn test_check_timedout_build() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec![
"make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[2]: Nothing to be done for 'install'.".to_owned(),
"make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[1]: Nothing to be done for 'install-target'.".to_owned(),
"make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'".to_owned(),
"removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'".to_owned(),
"post-installation fixup".to_owned(),
"building of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' timed out after 1 seconds".to_owned(),
"error: build of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' failed".to_owned(),
],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: None,
status: BuildStatus::TimedOut,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "foo on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
"https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "Timed out, unknown build status".to_string(),
summary: "Attempted: foo
Build timed out."
.to_string(),
text: Some(
"## Partial log
```
make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[2]: Nothing to be done for 'install'.
make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[1]: Nothing to be done for 'install-target'.
make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'
removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'
post-installation fixup
building of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' timed out after 1 seconds
error: build of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' failed
```"
.to_string()
),
annotations: None,
images: None,
})
}
);
}
#[test]
pub fn test_check_passing_build_unspecified_attributes() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec![
"make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[2]: Nothing to be done for 'install'.".to_owned(),
"make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[1]: Nothing to be done for 'install-target'.".to_owned(),
"make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'".to_owned(),
"removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'".to_owned(),
"post-installation fixup".to_owned(),
"strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip".to_owned(),
"patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
"/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Success,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "(unknown attributes) on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Success),
details_url: Some(
"https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "Success".to_string(),
summary: "".to_string(),
text: Some(
"## Partial log
```
make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[2]: Nothing to be done for 'install'.
make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[1]: Nothing to be done for 'install-target'.
make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'
removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'
post-installation fixup
strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip
patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
```"
.to_string()
),
annotations: None,
images: None,
})
}
);
}
#[test]
pub fn test_check_failing_build_unspecified_attributes() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec![
"make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[2]: Nothing to be done for 'install'.".to_owned(),
"make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'".to_owned(),
"make[1]: Nothing to be done for 'install-target'.".to_owned(),
"make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'".to_owned(),
"removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'".to_owned(),
"post-installation fixup".to_owned(),
"strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip".to_owned(),
"patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
"/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1".to_owned(),
],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Failure,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "(unknown attributes) on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
"https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "Failure".to_string(),
summary: "".to_string(),
text: Some(
"## Partial log
```
make[2]: Entering directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[2]: Nothing to be done for 'install'.
make[2]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1/readline'
make[1]: Nothing to be done for 'install-target'.
make[1]: Leaving directory '/private/tmp/nix-build-gdb-8.1.drv-0/gdb-8.1'
removed '/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1/share/info/bfd.info'
post-installation fixup
strip is /nix/store/5a88zk3jgimdmzg8rfhvm93kxib3njf9-cctools-binutils-darwin/bin/strip
patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
/nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29-gdb-8.1
```"
.to_string()
),
annotations: None,
images: None,
})
}
);
}
#[test]
pub fn test_check_no_attempt() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec!["foo".to_owned()],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: None,
skipped_attrs: Some(vec!["not-attempted".to_owned()]),
status: BuildStatus::Skipped,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "not-attempted on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Skipped),
details_url: Some("https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid".to_string()),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "No attempt".to_string(),
summary: "The following builds were skipped because they don\'t evaluate on x86_64-linux: not-attempted
".to_string(),
text: Some("## Partial log
```
foo
```".to_string()),
annotations: None,
images: None,
})
}
);
}
#[test]
pub fn test_check_no_attempt_no_log() {
let result = LegacyBuildResult {
repo: Repo {
clone_url: "https://github.com/nixos/nixpkgs.git".to_owned(),
full_name: "NixOS/nixpkgs".to_owned(),
owner: "NixOS".to_owned(),
name: "nixpkgs".to_owned(),
},
pr: Change {
head_sha: "abc123".to_owned(),
number: 2345,
target_branch: Some("master".to_owned()),
},
output: vec![],
attempt_id: "neatattemptid".to_owned(),
request_id: "bogus-request-id".to_owned(),
system: "x86_64-linux".to_owned(),
attempted_attrs: None,
skipped_attrs: Some(vec!["not-attempted".to_owned()]),
status: BuildStatus::Skipped,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
assert_eq!(
result_to_check(&result, timestamp),
CheckRunOptions {
name: "not-attempted on x86_64-linux".to_string(),
actions: None,
started_at: None,
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Skipped),
details_url: Some("https://logs.ofborg.org/?key=nixos/nixpkgs.2345&attempt_id=neatattemptid".to_string()),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
title: "No attempt".to_string(),
summary: "The following builds were skipped because they don\'t evaluate on x86_64-linux: not-attempted
".to_string(),
text: Some("No partial log is available.".to_string()),
annotations: None,
images: None,
})
}
);
}
}

View file

@ -2,8 +2,6 @@ pub mod build;
pub mod eval;
pub mod evaluate;
pub mod evaluationfilter;
pub mod githubcommentfilter;
pub mod githubcommentposter;
pub mod log_message_collector;
pub mod pastebin_collector;
pub mod statscollector;

View file

@ -80,7 +80,7 @@ impl CommitStatus {
#[derive(Debug)]
pub enum CommitStatusError {
ExpiredCreds(hubcaps::Error),
MissingSha(hubcaps::Error),
Error(hubcaps::Error),
ExpiredCreds(()),
MissingSha(()),
Error(()),
}

View file

@ -1,361 +0,0 @@
use std::collections::HashSet;
use futures_util::{future::BoxFuture, FutureExt};
use hubcaps::pulls::PullListOptions;
use crate::{
message::{buildresult::BuildStatus, Change, Repo},
vcs::{
commit_status::CommitStatusError,
generic::{
Account, CheckRunOptions, CheckRunState, Conclusion, Issue, Repository, State,
VersionControlSystemAPI,
},
},
};
impl From<BuildStatus> for hubcaps::checks::Conclusion {
fn from(status: BuildStatus) -> hubcaps::checks::Conclusion {
match status {
BuildStatus::Skipped => hubcaps::checks::Conclusion::Skipped,
BuildStatus::Success => hubcaps::checks::Conclusion::Success,
BuildStatus::Failure => hubcaps::checks::Conclusion::Neutral,
BuildStatus::HashMismatch => hubcaps::checks::Conclusion::Failure,
BuildStatus::TimedOut => hubcaps::checks::Conclusion::Neutral,
BuildStatus::UnexpectedError { .. } => hubcaps::checks::Conclusion::Neutral,
}
}
}
impl From<CheckRunState> for hubcaps::checks::CheckRunState {
fn from(val: CheckRunState) -> Self {
match val {
CheckRunState::Runnable | CheckRunState::Scheduled => {
hubcaps::checks::CheckRunState::Queued
}
CheckRunState::Running => hubcaps::checks::CheckRunState::InProgress,
CheckRunState::Completed => hubcaps::checks::CheckRunState::Completed,
}
}
}
impl From<Conclusion> for hubcaps::checks::Conclusion {
fn from(val: Conclusion) -> Self {
match val {
Conclusion::Skipped => hubcaps::checks::Conclusion::Skipped,
Conclusion::Success => hubcaps::checks::Conclusion::Success,
Conclusion::Failure => hubcaps::checks::Conclusion::Failure,
Conclusion::Neutral => hubcaps::checks::Conclusion::Neutral,
Conclusion::Cancelled => hubcaps::checks::Conclusion::Cancelled,
Conclusion::TimedOut => hubcaps::checks::Conclusion::TimedOut,
Conclusion::ActionRequired => hubcaps::checks::Conclusion::ActionRequired,
}
}
}
impl From<CheckRunOptions> for hubcaps::checks::CheckRunOptions {
fn from(val: CheckRunOptions) -> Self {
hubcaps::checks::CheckRunOptions {
name: val.name,
head_sha: val.head_sha,
details_url: val.details_url,
external_id: val.external_id,
status: val.status.map(|c| c.into()),
started_at: val.started_at,
conclusion: val.conclusion.map(|c| c.into()),
completed_at: val.completed_at,
output: None,
actions: None,
}
}
}
impl From<State> for hubcaps::statuses::State {
fn from(val: State) -> Self {
match val {
State::Pending => hubcaps::statuses::State::Pending,
State::Error => hubcaps::statuses::State::Error,
State::Failure => hubcaps::statuses::State::Failure,
State::Success => hubcaps::statuses::State::Success,
}
}
}
pub struct GitHubAPI {
client: hubcaps::Github,
}
impl GitHubAPI {
pub fn new(client: hubcaps::Github) -> Self {
Self { client }
}
}
impl From<hubcaps::repositories::Repository> for Repository {
fn from(_val: hubcaps::repositories::Repository) -> Self {
Repository {}
}
}
impl From<hubcaps::pulls::Pull> for Change {
fn from(val: hubcaps::pulls::Pull) -> Self {
Change {
head_sha: val.head.sha,
number: val.number,
target_branch: Some(val.base.label),
}
}
}
impl From<hubcaps::users::User> for Account {
fn from(val: hubcaps::users::User) -> Self {
Account {
username: val.login,
}
}
}
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 Issue {
fn from_github_issue(repo: Repo, issue: hubcaps::issues::Issue) -> Self {
Self {
number: issue.number,
title: issue.title,
repo,
state: match issue.state.as_str() {
"closed" => crate::vcs::generic::IssueState::Closed,
"open" => crate::vcs::generic::IssueState::Open,
_ => panic!("unsupported issue state"),
},
created_by: issue.user.into(),
}
}
}
impl VersionControlSystemAPI for GitHubAPI {
fn get_repository(&self, repo: &crate::message::Repo) -> Repository {
self.client
.repo(repo.owner.clone(), repo.name.clone())
.into()
}
fn get_changes(&self, repo: &crate::message::Repo) -> BoxFuture<Vec<crate::message::Change>> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
let changes = repo.pulls();
async move {
changes
.list(&PullListOptions::default())
.await
.expect("Failed to obtain changes")
.into_iter()
.map(|pr| pr.into())
.collect()
}
.boxed()
}
fn get_change(&self, repo: &crate::message::Repo, number: u64) -> BoxFuture<Option<Change>> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
let changes = repo.pulls();
let change = changes.get(number);
async move {
Some(
change
.get()
.await
.unwrap_or_else(|_| panic!("Failed to obtain change {}", number))
.into(),
)
}
.boxed()
}
fn get_issue(
&self,
repo: &crate::message::Repo,
number: u64,
) -> BoxFuture<Result<crate::vcs::generic::Issue, String>> {
let repository = self.client.repo(repo.owner.clone(), repo.name.clone());
let issue = repository.issue(number);
let repo = repo.clone();
async move {
Ok(Issue::from_github_issue(
repo,
issue
.get()
.await
.unwrap_or_else(|_| panic!("Failed to obtain issue reference {}", number)),
))
}
.boxed()
}
fn update_labels(
&self,
repo: &crate::message::Repo,
number: u64,
add: &[String],
remove: &[String],
) -> BoxFuture<()> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
let issue_ref = repo.issue(number);
let label_ref = issue_ref.labels();
let add = add.to_owned();
let remove = remove.to_owned();
async move {
let issue = issue_ref.get().await.expect("Failed to obtain issue");
let existing: HashSet<String> = issue
.labels
.iter()
.map(|label| label.name.clone())
.collect();
let to_add: Vec<String> = add
.into_iter()
.filter(|l| !existing.contains::<str>(l.as_ref()))
.collect();
let to_remove: Vec<String> = remove
.into_iter()
.filter(|l| existing.contains::<str>(l.as_ref()))
.collect();
tracing::info!(
"Labelling issue #{}: +{:?}, -{:?}, = {:?}",
issue.number,
to_add,
to_remove,
existing
);
label_ref
.add(to_add.iter().map(|s| s as &str).collect())
.await
.unwrap_or_else(|_| panic!("Failed to add labels {:?} to issue #{}",
to_add, issue.number));
for label in to_remove {
label_ref.remove(&label).await.unwrap_or_else(|_| panic!("Failed to remove label {:?} from issue #{}",
label, issue.number));
}
}
.boxed()
}
fn request_reviewers(
&self,
repo: &crate::message::Repo,
number: u64,
entity_reviewers: Vec<String>,
team_reviewers: Vec<String>,
) -> BoxFuture<()> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
let pulls = repo.pulls();
let pull = pulls.get(number);
async move {
pull.review_requests()
.create(&hubcaps::review_requests::ReviewRequestOptions {
reviewers: entity_reviewers,
team_reviewers,
})
.await
.expect("Failed to request reviewers");
}
.boxed()
}
fn get_existing_reviewers(
&self,
repo: &crate::message::Repo,
number: u64,
) -> BoxFuture<crate::vcs::generic::ChangeReviewers> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
let pulls = repo.pulls();
let pull = pulls.get(number);
async move {
let reviewers = pull
.review_requests()
.get()
.await
.expect("Failed to obtain reviewers");
crate::vcs::generic::ChangeReviewers {
entity_reviewers: reviewers.users.into_iter().map(|u| u.login).collect(),
team_reviewers: reviewers.teams.into_iter().map(|t| t.name).collect(),
}
}
.boxed()
}
fn create_commit_statuses(
&self,
repo: &crate::message::Repo,
sha: String,
state: crate::vcs::generic::State,
context: String,
description: String,
target_url: String,
) -> BoxFuture<Result<(), crate::vcs::commit_status::CommitStatusError>> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
let api = repo.statuses();
async move {
api.create(
&sha,
&hubcaps::statuses::StatusOptions::builder(state.into())
.context(context)
.description(description)
.target_url(target_url)
.build(),
)
.await
.map(|_| ())
.map_err(|err| err.into())
}
.boxed()
}
fn create_check_statuses(
&self,
repo: &crate::message::Repo,
checks: Vec<crate::vcs::generic::CheckRunOptions>,
) -> BoxFuture<()> {
let repo = self.client.repo(repo.owner.clone(), repo.name.clone());
async move {
for check in checks {
match repo.checkruns().create(&check.into()).await {
Ok(_) => tracing::debug!("Sent check update"),
Err(e) => tracing::warn!("Failed to send check update: {:?}", e),
}
}
}
.boxed()
}
}

View file

@ -1 +0,0 @@
pub mod compat;

View file

@ -1,4 +1,3 @@
pub mod commit_status;
pub mod generic;
pub mod gerrit;
pub mod github;