diff --git a/crates/runner-integration/src/connect.rs b/crates/runner-integration/src/connect.rs index b303ee4..3081c63 100644 --- a/crates/runner-integration/src/connect.rs +++ b/crates/runner-integration/src/connect.rs @@ -7,13 +7,22 @@ use crate::proto::runner::v1::{ RegisterResponse, UpdateLogRequest, UpdateLogResponse, UpdateTaskRequest, UpdateTaskResponse, }; +/// Authentication mode for connect-rpc calls. +/// Forgejo uses custom `x-runner-token` / `x-runner-uuid` headers, +/// NOT `Authorization: Bearer`. +pub enum Auth<'a> { + /// Registration uses only the one-time registration token. + Registration(&'a str), + /// Post-registration uses the runner's UUID + token pair. + Runner { uuid: &'a str, token: &'a str }, +} + /// 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. @@ -34,27 +43,35 @@ impl ConnectClient { } /// Execute a unary connect-rpc call. - #[instrument(skip(self, req_msg, token), fields(method = %method))] + #[instrument(skip(self, req_msg, auth), fields(method = %method))] async fn call( &self, method: &str, req_msg: &Req, - token: &str, + auth: Auth<'_>, ) -> 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 + let mut req = self .http .post(&url) - .header("Content-Type", "application/proto") - .header("Authorization", format!("Bearer {}", token)) - .body(body) - .send() - .await - .into_diagnostic()?; + .header("Content-Type", "application/proto"); + + // Forgejo runner API uses custom auth headers + match auth { + Auth::Registration(token) => { + req = req.header("x-runner-token", token); + } + Auth::Runner { uuid, token } => { + req = req.header("x-runner-token", token); + req = req.header("x-runner-uuid", uuid); + } + } + + let resp = req.body(body).send().await.into_diagnostic()?; let status = resp.status(); if !status.is_success() { @@ -72,48 +89,85 @@ impl ConnectClient { } /// Register this runner with the Forgejo instance. - /// Uses the one-time registration token (not the runner token). + /// Uses the one-time registration token. pub async fn register( &self, req: &RegisterRequest, registration_token: &str, ) -> Result { - self.call("Register", req, registration_token).await + self.call("Register", req, Auth::Registration(registration_token)) + .await } /// Declare runner version and labels after registration. pub async fn declare( &self, req: &DeclareRequest, + uuid: &str, runner_token: &str, ) -> Result { - self.call("Declare", req, runner_token).await + self.call( + "Declare", + req, + Auth::Runner { + uuid, + token: runner_token, + }, + ) + .await } /// Long-poll for the next available task. pub async fn fetch_task( &self, req: &FetchTaskRequest, + uuid: &str, runner_token: &str, ) -> Result { - self.call("FetchTask", req, runner_token).await + self.call( + "FetchTask", + req, + Auth::Runner { + uuid, + token: runner_token, + }, + ) + .await } /// Update a task's state (running, success, failure, etc.). pub async fn update_task( &self, req: &UpdateTaskRequest, + uuid: &str, runner_token: &str, ) -> Result { - self.call("UpdateTask", req, runner_token).await + self.call( + "UpdateTask", + req, + Auth::Runner { + uuid, + token: runner_token, + }, + ) + .await } /// Upload log lines for a task. pub async fn update_log( &self, req: &UpdateLogRequest, + uuid: &str, runner_token: &str, ) -> Result { - self.call("UpdateLog", req, runner_token).await + self.call( + "UpdateLog", + req, + Auth::Runner { + uuid, + token: runner_token, + }, + ) + .await } } diff --git a/crates/runner-integration/src/poller.rs b/crates/runner-integration/src/poller.rs index d9aae17..00c6a04 100644 --- a/crates/runner-integration/src/poller.rs +++ b/crates/runner-integration/src/poller.rs @@ -51,7 +51,7 @@ pub async fn run( // Long-poll for a task let req = FetchTaskRequest { tasks_version }; let resp = tokio::select! { - r = client.fetch_task(&req, &state.identity.token) => r, + r = client.fetch_task(&req, &state.identity.uuid, &state.identity.token) => r, _ = shutdown.changed() => { info!("poller shutting down (fetching task)"); break; @@ -189,7 +189,7 @@ async fn report_running(client: &ConnectClient, state: &RunnerState, task_id: i6 }), outputs: Default::default(), }; - client.update_task(&req, &state.identity.token).await?; + client.update_task(&req, &state.identity.uuid, &state.identity.token).await?; Ok(()) } @@ -213,7 +213,7 @@ async fn report_failure( }), outputs: Default::default(), }; - client.update_task(&req, &state.identity.token).await?; + client.update_task(&req, &state.identity.uuid, &state.identity.token).await?; // Also send the error message as a log line let log_req = crate::proto::runner::v1::UpdateLogRequest { @@ -228,7 +228,7 @@ async fn report_failure( }], no_more: true, }; - client.update_log(&log_req, &state.identity.token).await?; + client.update_log(&log_req, &state.identity.uuid, &state.identity.token).await?; Ok(()) } diff --git a/crates/runner-integration/src/registration.rs b/crates/runner-integration/src/registration.rs index 196220c..aa32f5d 100644 --- a/crates/runner-integration/src/registration.rs +++ b/crates/runner-integration/src/registration.rs @@ -25,7 +25,7 @@ pub async fn ensure_registered( "loaded existing runner registration" ); // Re-declare labels on every startup so Forgejo stays in sync - declare(client, &identity.token, labels).await?; + declare(client, &identity.uuid, &identity.token, labels).await?; return Ok(identity); } @@ -65,17 +65,22 @@ pub async fn ensure_registered( info!(uuid = %identity.uuid, id = identity.id, "runner registered successfully"); // Declare labels after fresh registration - declare(client, &identity.token, labels).await?; + declare(client, &identity.uuid, &identity.token, labels).await?; Ok(identity) } -async fn declare(client: &ConnectClient, runner_token: &str, labels: &[String]) -> Result<()> { +async fn declare( + client: &ConnectClient, + uuid: &str, + runner_token: &str, + labels: &[String], +) -> Result<()> { let req = DeclareRequest { version: VERSION.to_string(), labels: labels.to_vec(), }; - client.declare(&req, runner_token).await?; + client.declare(&req, uuid, runner_token).await?; info!(labels = ?labels, "declared runner labels"); Ok(()) } diff --git a/crates/runner-integration/src/reporter.rs b/crates/runner-integration/src/reporter.rs index 88108bc..f6b9efc 100644 --- a/crates/runner-integration/src/reporter.rs +++ b/crates/runner-integration/src/reporter.rs @@ -176,7 +176,7 @@ async fn report_to_forgejo( outputs: Default::default(), }; - client.update_task(&req, &state.identity.token).await?; + client.update_task(&req, &state.identity.uuid, &state.identity.token).await?; info!( request_id = %jobres.request_id,