1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/sourcehut-mirror-bridge synced 2024-12-21 22:32:19 +00:00

Compare commits

...

2 commits

Author SHA1 Message Date
Ash Keel
02fc03878d
add build pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-13 14:20:13 +01:00
Ash Keel
7058bf90b2
MVP! 2023-11-13 14:15:11 +01:00
10 changed files with 438 additions and 173 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
target
LICENSE
README.md
.env

43
.drone.yml Normal file
View file

@ -0,0 +1,43 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: publish sha
image: banzaicloud/drone-kaniko
settings:
registry: registry.fromouter.space
repo: uchumoe/sourcehut-mirror-bridge
cache: true
username:
from_secret: docker_username
password:
from_secret: docker_password
tags:
- latest
- ${DRONE_COMMIT_SHA}
when:
event:
- push
- name: publish tag
image: banzaicloud/drone-kaniko
settings:
registry: registry.fromouter.space
repo: uchumoe/sourcehut-mirror-bridge
cache: true
username:
from_secret: docker_username
password:
from_secret: docker_password
tags:
- ${DRONE_COMMIT_BRANCH}
when:
event:
- tag
---
kind: signature
hmac: 925643c1816fd7df3823079bc98a79836e34e4641b8328ae0bc3eae37923ff84
...

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
.env

296
Cargo.lock generated
View file

@ -68,6 +68,15 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
@ -177,7 +186,7 @@ dependencies = [
"log",
"parking",
"polling",
"rustix",
"rustix 0.37.27",
"slab",
"socket2",
"waker-fn",
@ -271,6 +280,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "block-buffer"
version = "0.9.0"
@ -360,18 +375,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"windows-sys 0.45.0",
]
[[package]]
name = "const_fn"
version = "0.4.9"
@ -456,6 +459,16 @@ dependencies = [
"cipher",
]
[[package]]
name = "ctrlc"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf"
dependencies = [
"nix",
"windows-sys",
]
[[package]]
name = "cynic"
version = "3.2.2"
@ -550,6 +563,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "either"
version = "1.9.0"
@ -557,10 +576,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encode_unicode"
version = "0.3.6"
name = "env_logger"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "envy"
@ -578,7 +604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
dependencies = [
"libc",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@ -826,6 +852,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "ident_case"
version = "1.0.1"
@ -848,19 +880,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
[[package]]
name = "insta"
version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"similar",
"yaml-rust",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -878,7 +897,18 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix 0.38.21",
"windows-sys",
]
[[package]]
@ -905,30 +935,24 @@ dependencies = [
"log",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "linux-raw-sys"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "log"
version = "0.4.20"
@ -969,6 +993,17 @@ dependencies = [
"adler",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"libc",
]
[[package]]
name = "num-traits"
version = "0.2.17"
@ -1055,13 +1090,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
dependencies = [
"autocfg",
"bitflags",
"bitflags 1.3.2",
"cfg-if",
"concurrent-queue",
"libc",
"log",
"pin-project-lite",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@ -1220,6 +1255,35 @@ dependencies = [
"syn 2.0.39",
]
[[package]]
name = "regex"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "ring"
version = "0.17.5"
@ -1231,7 +1295,7 @@ dependencies = [
"libc",
"spin",
"untrusted",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
@ -1249,12 +1313,25 @@ version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
"linux-raw-sys 0.3.8",
"windows-sys",
]
[[package]]
name = "rustix"
version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys 0.4.11",
"windows-sys",
]
[[package]]
@ -1398,12 +1475,6 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "similar"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597"
[[package]]
name = "slab"
version = "0.4.9"
@ -1429,10 +1500,14 @@ version = "0.1.0"
dependencies = [
"anyhow",
"argh",
"base64 0.21.5",
"ctrlc",
"cynic",
"cynic-codegen",
"dotenvy",
"env_logger",
"envy",
"insta",
"log",
"semver 1.0.20",
"serde",
"serde_json",
@ -1564,6 +1639,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.50"
@ -1890,43 +1974,28 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
"windows-targets",
]
[[package]]
@ -1935,104 +2004,53 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

View file

@ -7,14 +7,18 @@ license = "AGPL-3.0-only"
[dependencies]
anyhow = "1"
argh = "0.1"
base64 = "0.21.5"
ctrlc = { version = "3", features = ["termination"] }
cynic = { version = "3", features = ["http-surf"] }
dotenvy = "0.15"
envy = "0.4"
semver = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tiny_http = "0.12"
log = "0.4"
env_logger = "0.10"
ureq = { version = "2", features = ["json"] }
[build-dependencies]
cynic-codegen = { version = "3" }
insta = "1"

20
Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM public.ecr.aws/docker/library/rust:1-alpine as builder
RUN rustup default stable
RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconf git
# Set `SYSROOT` to a dummy path (default is /usr) because pkg-config-rs *always*
# links those located in that path dynamically but we want static linking, c.f.
# https://github.com/rust-lang/pkg-config-rs/blob/54325785816695df031cef3b26b6a9a203bbc01b/src/lib.rs#L613
ENV SYSROOT=/dummy
WORKDIR /wd
COPY . /wd
RUN cargo build --release --target=x86_64-unknown-linux-musl
FROM public.ecr.aws/docker/library/alpine
ARG version=unknown
ARG release=unreleased
COPY --from=builder /wd/target/x86_64-unknown-linux-musl/release/sourcehut-mirror-bridge /usr/local/bin/
CMD ["sourcehut-mirror-bridge"]

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# sourcehut-mirror-bridge
SourceHut to Gitea active mirror bridge. Uses Soucehut GQL APIs to set up a webhook and manually sync a downstream Gitea/Forgejo repository set as a mirror.
Use case: You're running Drone CI or others linked to a Gitea instance and you want them to run as soon as possible.

49
src/gitea.rs Normal file
View file

@ -0,0 +1,49 @@
use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use log::debug;
pub struct GiteaAPI {
api_endpoint: String,
auth: String,
}
impl GiteaAPI {
pub fn new(api_endpoint: &str, auth: &str) -> Self {
Self {
api_endpoint: api_endpoint.to_string(),
auth: basic_auth(auth),
}
}
pub fn check_repositories(&self, project_ids: &[&str]) -> Result<()> {
for repo in project_ids {
let api_url = format!("{}/v1/repos/{}", self.api_endpoint, repo);
let res = ureq::get(&api_url)
.set("Authorization", &self.auth)
.call()?;
if res.status() != 200 {
let body = res.into_string()?;
return Err(anyhow::anyhow!(
"Error fetching repository {}: {}",
repo,
body
));
}
debug!("Repository {} exists", repo);
}
Ok(())
}
pub fn sync(&self, project_id: &str) -> Result<()> {
let api_url = format!("{}/v1/repos/{}/mirror-sync", &self.api_endpoint, project_id);
ureq::post(&api_url)
.set("Authorization", &self.auth)
.call()?;
debug!("Synced repository {}", project_id);
Ok(())
}
}
fn basic_auth(auth: &str) -> String {
format!("Basic {}", general_purpose::STANDARD.encode(auth))
}

View file

@ -1,8 +1,14 @@
use anyhow::{anyhow, Result};
use gitea::GiteaAPI;
use log::{debug, error, info};
use serde::Deserialize;
use tiny_http::{Header, Server};
use std::{collections::HashMap, process::exit, sync::Arc};
use tiny_http::{Request, Response, Server};
mod webhook;
use crate::sourcehut::SourcehutAPI;
mod gitea;
mod sourcehut;
fn default_bind() -> String {
"0.0.0.0:8000".to_string()
@ -14,33 +20,120 @@ fn default_sourcehut_api() -> String {
#[derive(Deserialize, Debug)]
struct Config {
/// Bind address
#[serde(default = "default_bind")]
bind: String,
/// Sourcehut API endpoint
#[serde(default = "default_sourcehut_api")]
sourcehut_api: String,
/// Sourcehut API token (from https://meta.sr.ht/oauth2, needs `git.sr.ht/REPOSITORY` grant)
#[serde(default = "default_sourcehut_api")]
sourcehut_token: String,
/// Base URL for Gitea API (without the /v1)
gitea_api_base: String,
/// username:password for basic auth
gitea_auth: String,
/// URL to set as sourcehut webhook target where this service is reacheable
url: String,
/// Comma separated list of repository map from sourcehut to Gitea e.g. "sourcehutrepo:owner/gitearepo"
repositories: String,
}
fn main() -> Result<()> {
let _ = dotenvy::dotenv();
env_logger::init();
let config = envy::prefixed("MIRROR_").from_env::<Config>()?;
// Parse repository mapping into a hashmap
let repository_map: HashMap<&str, &str> = config
.repositories
.split(",")
.map(|s| s.split_once(":").unwrap())
.collect();
if repository_map.is_empty() {
return Err(anyhow!("No repositories provided"));
}
debug!("Repository map: {:?}", repository_map);
// Set up Gitea API and check that provided repositories exist
let gitea = GiteaAPI::new(&config.gitea_api_base, &config.gitea_auth);
let repositories = repository_map.values().map(|&s| s).collect::<Vec<_>>();
gitea.check_repositories(&repositories)?;
// Set up webhooks via GQL API to sourcehut
let mut srht_api = webhook::SourceHutAPI::new(&config.sourcehut_api, &config.sourcehut_token);
srht_api.init_webhook(&config.url)?;
let mut srht = SourcehutAPI::new(&config.sourcehut_api, &config.sourcehut_token);
srht.init_webhook(&config.url)?;
let api = Arc::new(srht);
let r = api.clone();
// Set up termination handler to delete webhook
ctrlc::set_handler(move || {
_ = r.delete_webhook();
exit(0)
})
.expect("Error setting termination handler");
// Listen for webhooks and handle them
let server = Server::http(config.bind).map_err(|err| anyhow!(err))?;
let json = Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap();
for request in server.incoming_requests() {}
for request in server.incoming_requests() {
debug!("Received request from {:?}", request.remote_addr());
match handle_request(request) {
Ok(Some(repository)) => {
// Get mapped repository, if it exists
if let Some(mapped) = repository_map.get(repository.as_str()) {
info!("Syncing repository {}", mapped);
if let Err(err) = gitea.sync(&mapped) {
error!("Error syncing repository: {}", err);
}
}
}
Err(err) => {
error!("Error handling webhook: {}", err);
}
_ => {
// Invalid request, ignore
}
}
}
Ok(())
}
fn handle_request(mut request: Request) -> Result<Option<String>> {
// Make sure the content type is JSON
let is_json = request
.headers()
.iter()
.find(|h| h.field.as_str() == "Content-Type" && h.value == "application/json")
.ok_or_else(|| anyhow!("Invalid Content-Type header"));
match is_json {
Ok(_) => {
let mut content = String::new();
request.as_reader().read_to_string(&mut content)?;
let repository = SourcehutAPI::read_webhook_payload(&content)?;
request.respond(Response::empty(204))?;
return Ok(Some(repository.name));
}
Err(err) => {
debug!(
"Rejecting request because of invalid Content-Type header: {}",
err
);
request.respond(Response::from_string(err.to_string()).with_status_code(400))?
}
}
Ok(None)
}

View file

@ -1,5 +1,6 @@
use anyhow::Result;
use cynic::{GraphQlResponse, MutationBuilder};
use log::error;
#[cynic::schema("sourcehut")]
mod schema {}
@ -14,13 +15,9 @@ struct CreateWebhookVariables {
struct CreateWebhook {
#[arguments(config: { events: ["REPO_UPDATE"], query: r#"query {
webhook {
uuid
event
date
... on RepositoryEvent {
repository {
name
updated
}
}
}
@ -42,18 +39,44 @@ struct DeleteWebhookVariables {
#[cynic(graphql_type = "Mutation", variables = "DeleteWebhookVariables")]
struct DeleteWebhook {
#[arguments(id: $id)]
#[allow(dead_code)]
delete_user_webhook: WebhookSubscription,
}
pub struct SourceHutAPI {
#[derive(cynic::QueryFragment, Debug)]
struct RepositoryEvent {
repository: Repository,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Repository {
pub name: String,
}
#[derive(serde::Deserialize, Debug)]
struct WebhookEventReceived {
webhook: RepositoryEvent,
}
#[derive(cynic::Enum, Clone, Copy, Debug)]
enum WebhookEvent {
RepoCreated,
RepoUpdate,
RepoDeleted,
}
#[derive(cynic::Scalar, Debug, Clone)]
pub struct Time(pub String);
pub struct SourcehutAPI {
api_endpoint: String,
token: String,
webhook_id: Option<i32>,
}
fn parse_gql_response(response: GraphQlResponse<WebhookSubscription>) -> Result<i32> {
fn parse_gql_response<T>(response: GraphQlResponse<T>) -> Result<T> {
match response.data {
Some(data) => Ok(data.id),
Some(data) => Ok(data),
None => {
// Check for errors
match response.errors {
@ -68,7 +91,7 @@ fn parse_gql_response(response: GraphQlResponse<WebhookSubscription>) -> Result<
}
}
impl SourceHutAPI {
impl SourcehutAPI {
pub fn new(api_endpoint: &str, token: &str) -> Self {
Self {
api_endpoint: api_endpoint.to_string(),
@ -83,32 +106,37 @@ impl SourceHutAPI {
});
let response = ureq::post(&self.api_endpoint)
.set("Authorization", &format!("Bearer {}", &self.token))
.send_json(&operation)?
.into_json::<GraphQlResponse<WebhookSubscription>>()?;
let webhook_id = parse_gql_response(response)?;
self.webhook_id = Some(webhook_id);
.send_json(operation)?
.into_json::<GraphQlResponse<CreateWebhook>>()?;
let webhook_ev = parse_gql_response(response)?;
self.webhook_id = Some(webhook_ev.create_user_webhook.id);
Ok(())
}
fn delete_webhook(&self, id: i32) -> Result<i32> {
pub fn delete_webhook(&self) -> Result<()> {
if let Some(id) = self.webhook_id {
let operation = DeleteWebhook::build(DeleteWebhookVariables { id });
let response = ureq::post(&self.api_endpoint)
.set("Authorization", &format!("Bearer {}", &self.token))
.send_json(&operation)?
.into_json::<GraphQlResponse<WebhookSubscription>>()?;
.send_json(operation)?
.into_json::<GraphQlResponse<DeleteWebhook>>()?;
parse_gql_response(response)
parse_gql_response(response)?;
}
Ok(())
}
pub fn read_webhook_payload(data: &str) -> Result<Repository> {
let data = serde_json::from_str::<GraphQlResponse<WebhookEventReceived>>(data)?;
parse_gql_response(data).map(|ev| ev.webhook.repository)
}
}
impl Drop for SourceHutAPI {
impl Drop for SourcehutAPI {
fn drop(&mut self) {
if let Some(id) = self.webhook_id {
if let Err(e) = self.delete_webhook(id) {
println!("Failed to delete webhook: {}", e);
}
if let Err(e) = self.delete_webhook() {
error!("Failed to delete webhook: {}", e);
}
}
}