solstice-ci/crates/runner-integration/src/connect.rs

120 lines
3.7 KiB
Rust
Raw Normal View History

use miette::{IntoDiagnostic, Result, miette};
use prost::Message;
use tracing::{debug, instrument};
use crate::proto::runner::v1::{
DeclareRequest, DeclareResponse, FetchTaskRequest, FetchTaskResponse, RegisterRequest,
RegisterResponse, UpdateLogRequest, UpdateLogResponse, UpdateTaskRequest, UpdateTaskResponse,
};
/// Connect-RPC client for the Forgejo Actions Runner API.
///
/// The Forgejo runner API uses the Connect protocol (HTTP/1.1 POST with raw
/// protobuf bodies), not standard gRPC framing. Each RPC maps to:
/// POST {base_url}/runner.v1.RunnerService/{Method}
/// Content-Type: application/proto
/// Authorization: Bearer {token}
pub struct ConnectClient {
http: reqwest::Client,
/// Base URL for the connect-rpc endpoint, e.g.
/// `https://forgejo.example.com/api/actions`
base_url: String,
}
impl ConnectClient {
pub fn new(base_url: impl Into<String>) -> Self {
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(90)) // long-poll needs generous timeout
.build()
.expect("failed to build HTTP client");
Self {
http,
base_url: base_url.into().trim_end_matches('/').to_string(),
}
}
/// Execute a unary connect-rpc call.
#[instrument(skip(self, req_msg, token), fields(method = %method))]
async fn call<Req: Message, Resp: Message + Default>(
&self,
method: &str,
req_msg: &Req,
token: &str,
) -> Result<Resp> {
let url = format!("{}/runner.v1.RunnerService/{}", self.base_url, method);
debug!(url = %url, "connect-rpc call");
let body = req_msg.encode_to_vec();
let resp = self
.http
.post(&url)
.header("Content-Type", "application/proto")
.header("Authorization", format!("Bearer {}", token))
.body(body)
.send()
.await
.into_diagnostic()?;
let status = resp.status();
if !status.is_success() {
let error_body = resp.text().await.unwrap_or_default();
return Err(miette!(
"connect-rpc {} failed: HTTP {} — {}",
method,
status,
error_body
));
}
let resp_bytes = resp.bytes().await.into_diagnostic()?;
Resp::decode(resp_bytes.as_ref()).into_diagnostic()
}
/// Register this runner with the Forgejo instance.
/// Uses the one-time registration token (not the runner token).
pub async fn register(
&self,
req: &RegisterRequest,
registration_token: &str,
) -> Result<RegisterResponse> {
self.call("Register", req, registration_token).await
}
/// Declare runner version and labels after registration.
pub async fn declare(
&self,
req: &DeclareRequest,
runner_token: &str,
) -> Result<DeclareResponse> {
self.call("Declare", req, runner_token).await
}
/// Long-poll for the next available task.
pub async fn fetch_task(
&self,
req: &FetchTaskRequest,
runner_token: &str,
) -> Result<FetchTaskResponse> {
self.call("FetchTask", req, runner_token).await
}
/// Update a task's state (running, success, failure, etc.).
pub async fn update_task(
&self,
req: &UpdateTaskRequest,
runner_token: &str,
) -> Result<UpdateTaskResponse> {
self.call("UpdateTask", req, runner_token).await
}
/// Upload log lines for a task.
pub async fn update_log(
&self,
req: &UpdateLogRequest,
runner_token: &str,
) -> Result<UpdateLogResponse> {
self.call("UpdateLog", req, runner_token).await
}
}