use std::path::Path; use miette::{IntoDiagnostic, Result, miette}; use tracing::{info, warn}; use crate::connect::ConnectClient; use crate::proto::runner::v1::{DeclareRequest, RegisterRequest}; use crate::state::RunnerIdentity; const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Load existing runner credentials from disk, or register a new runner. pub async fn ensure_registered( client: &ConnectClient, state_path: &str, registration_token: Option<&str>, runner_name: &str, labels: &[String], ) -> Result { // Try loading existing state if let Some(identity) = load_state(state_path) { info!( uuid = %identity.uuid, name = %identity.name, "loaded existing runner registration" ); // Re-declare labels on every startup so Forgejo stays in sync declare(client, &identity.uuid, &identity.token, labels).await?; return Ok(identity); } // No saved state — must register let token = registration_token.ok_or_else(|| { miette!( "no saved runner state at {state_path} and RUNNER_REGISTRATION_TOKEN is not set; \ cannot register with Forgejo" ) })?; info!(name = runner_name, "registering new runner with Forgejo"); let req = RegisterRequest { name: runner_name.to_string(), token: token.to_string(), version: VERSION.to_string(), labels: labels.to_vec(), ephemeral: false, ..Default::default() }; let resp = client.register(&req, token).await?; let runner = resp .runner .ok_or_else(|| miette!("Forgejo returned empty runner in RegisterResponse"))?; let identity = RunnerIdentity { id: runner.id, uuid: runner.uuid, token: runner.token, name: runner.name, registered_at: time::OffsetDateTime::now_utc().to_string(), }; save_state(state_path, &identity)?; info!(uuid = %identity.uuid, id = identity.id, "runner registered successfully"); // Declare labels after fresh registration declare(client, &identity.uuid, &identity.token, labels).await?; Ok(identity) } 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, uuid, runner_token).await?; info!(labels = ?labels, "declared runner labels"); Ok(()) } fn load_state(path: &str) -> Option { let p = Path::new(path); if !p.exists() { return None; } match std::fs::read_to_string(p) { Ok(data) => match serde_json::from_str::(&data) { Ok(id) => Some(id), Err(e) => { warn!(error = %e, path = %path, "failed to parse runner state; will re-register"); None } }, Err(e) => { warn!(error = %e, path = %path, "failed to read runner state; will re-register"); None } } } fn save_state(path: &str, identity: &RunnerIdentity) -> Result<()> { // Ensure parent directory exists if let Some(parent) = Path::new(path).parent() { std::fs::create_dir_all(parent).into_diagnostic()?; } let json = serde_json::to_string_pretty(identity).into_diagnostic()?; std::fs::write(path, json).into_diagnostic()?; info!(path = %path, "saved runner state"); Ok(()) }