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) -> 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( &self, method: &str, req_msg: &Req, token: &str, ) -> Result { 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 { 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 { 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 { 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 { 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 { self.call("UpdateLog", req, runner_token).await } }