use anyhow::{self, Error as AnyhowError};
use forge_core_deployment::{Deployment, DeploymentError};
use forge_core_server::{DeploymentImpl, routes};
use forge_core_utils::{
assets::asset_dir,
browser::open_browser,
port_file::write_port_file,
sentry::{self as sentry_utils, SentrySource, sentry_layer},
};
use sqlx::Error as SqlxError;
use strip_ansi_escapes::strip;
use thiserror::Error;
use tracing_subscriber::{EnvFilter, prelude::*};
#[derive(Debug, Error)]
pub enum AutomagikForgeError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Sqlx(#[from] SqlxError),
#[error(transparent)]
Deployment(#[from] DeploymentError),
#[error(transparent)]
Other(#[from] AnyhowError),
}
#[tokio::main]
async fn main() -> Result<(), AutomagikForgeError> {
dotenvy::dotenv().ok();
sentry_utils::init_once(SentrySource::Backend);
let log_level = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
let filter_string = format!(
"warn,server={level},services={level},db={level},executors={level},deployment={level},local_deployment={level},utils={level}",
level = log_level
);
let env_filter = EnvFilter::try_new(filter_string).expect("Failed to create tracing filter");
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().with_filter(env_filter))
.with(sentry_layer())
.init();
if !asset_dir().exists() {
std::fs::create_dir_all(asset_dir())?;
}
let deployment = DeploymentImpl::new().await?;
deployment.update_sentry_scope().await?;
deployment.cleanup_orphan_executions().await?;
deployment.backfill_before_head_commits().await?;
deployment.spawn_pr_monitor_service().await;
deployment
.track_if_analytics_allowed("session_start", serde_json::json!({}))
.await;
let deployment_for_cache = deployment.clone();
tokio::spawn(async move {
if let Err(e) = deployment_for_cache
.file_search_cache()
.warm_most_active(&deployment_for_cache.db().pool, 3)
.await
{
tracing::warn!("Failed to warm file search cache: {}", e);
}
});
let app_router = routes::router(deployment);
let port = std::env::var("BACKEND_PORT")
.or_else(|_| std::env::var("PORT"))
.ok()
.and_then(|s| {
let cleaned =
String::from_utf8(strip(s.as_bytes())).expect("UTF-8 after stripping ANSI");
cleaned.trim().parse::<u16>().ok()
})
.unwrap_or_else(|| {
tracing::info!("No PORT environment variable set, using port 0 for auto-assignment");
0
});
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let listener = tokio::net::TcpListener::bind(format!("{host}:{port}")).await?;
let actual_port = listener.local_addr()?.port();
if let Err(e) = write_port_file(actual_port).await {
tracing::warn!("Failed to write port file: {}", e);
}
tracing::info!("Server running on http://{host}:{actual_port}");
if !cfg!(debug_assertions) {
tracing::info!("Opening browser...");
tokio::spawn(async move {
if let Err(e) = open_browser(&format!("http://127.0.0.1:{actual_port}")).await {
tracing::warn!(
"Failed to open browser automatically: {}. Please open http://127.0.0.1:{} manually.",
e,
actual_port
);
}
});
}
axum::serve(listener, app_router).await?;
Ok(())
}