From 7a22798c23249b8acb92f833c566a24526277653 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 3 Mar 2026 14:30:44 -0300 Subject: [PATCH] Fix deployment module: add types, router, handlers and fix forgejo integration --- Cargo.toml | 1 + src/deployment/forgejo.rs | 227 ++++++++---------- src/deployment/handlers.rs | 175 ++++++++++++++ src/deployment/mod.rs | 464 +++---------------------------------- src/deployment/router.rs | 102 ++++++++ src/deployment/types.rs | 270 +++++++++++++++++++++ 6 files changed, 684 insertions(+), 555 deletions(-) create mode 100644 src/deployment/handlers.rs create mode 100644 src/deployment/router.rs create mode 100644 src/deployment/types.rs diff --git a/Cargo.toml b/Cargo.toml index 5b5b5b953..53d6453dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ dirs = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } +git2 = "0.19" hex = { workspace = true } hmac = { workspace = true } log = { workspace = true } diff --git a/src/deployment/forgejo.rs b/src/deployment/forgejo.rs index 22ad6e56e..d40c589ba 100644 --- a/src/deployment/forgejo.rs +++ b/src/deployment/forgejo.rs @@ -1,9 +1,10 @@ -use git2::{Repository, Oid, Signature, Time}; +use git2::{Repository, Signature}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; +use std::path::Path; -use super::{DeploymentError, GeneratedApp}; +use super::{DeploymentError, GeneratedApp, GeneratedFile}; +use super::types::{AppType, DeploymentEnvironment}; pub struct ForgejoClient { base_url: String, @@ -129,17 +130,20 @@ impl ForgejoClient { Ok(oid.to_string()) } - /// Create CI/CD workflow for the app + /// Create CI/CD workflow for the app based on Phase 2.5 app types pub async fn create_cicd_workflow( &self, repo_url: &str, - app_type: AppType, - build_config: BuildConfig, + app_type: &AppType, + environment: &DeploymentEnvironment, ) -> Result<(), DeploymentError> { let workflow = match app_type { - AppType::Htmx => self.generate_htmx_workflow(build_config), - AppType::React => self.generate_react_workflow(build_config), - AppType::Vue => self.generate_vue_workflow(build_config), + AppType::GbNative { .. } => self.generate_gb_native_workflow(environment), + AppType::Custom { framework, node_version, build_command, output_directory } => { + self.generate_custom_workflow(framework, node_version.as_deref().unwrap_or("20"), + build_command.as_deref().unwrap_or("npm run build"), + output_directory.as_deref().unwrap_or("dist"), environment) + } }; // Create workflow file @@ -193,106 +197,11 @@ impl ForgejoClient { } } - fn generate_htmx_workflow(&self, _config: BuildConfig) -> String { - r#"name: Deploy HTMX App -on: - push: - branches: [main, develop] -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - - name: Install dependencies - run: npm ci - - name: Build - run: npm run build - - - name: Deploy to server - run: | - echo "Deploying HTMX app to production..." - # Add deployment commands here -"#.to_string() - } - - fn generate_react_workflow(&self, _config: BuildConfig) -> String { - r#"name: Deploy React App - -on: - push: - branches: [main, develop] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build React app - run: npm run build - - - name: Run tests - run: npm test - - - name: Deploy to production - run: | - echo "Deploying React app to production..." - # Add deployment commands here -"#.to_string() - } - - fn generate_vue_workflow(&self, _config: BuildConfig) -> String { - r#"name: Deploy Vue App - -on: - push: - branches: [main, develop] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build Vue app - run: npm run build - - - name: Run tests - run: npm test - - - name: Deploy to production - run: | - echo "Deploying Vue app to production..." - # Add deployment commands here -"#.to_string() - } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -318,29 +227,7 @@ struct CreateRepoRequest { readme: Option, } -#[derive(Debug, Clone, Copy)] -pub enum AppType { - Htmx, - React, - Vue, -} - -#[derive(Debug, Clone)] -pub struct BuildConfig { - pub node_version: String, - pub build_command: String, - pub output_dir: String, -} - -impl Default for BuildConfig { - fn default() -> Self { - Self { - node_version: "20".to_string(), - build_command: "npm run build".to_string(), - output_dir: "dist".to_string(), - } - } -} +// AppType and related types are now defined in types.rs #[derive(Debug)] pub enum ForgejoError { @@ -361,4 +248,90 @@ impl std::fmt::Display for ForgejoError { } } +// ============================================================================= +// CI/CD Workflow Generation for Phase 2.5 +// ============================================================================= + +impl ForgejoClient { + /// Generate CI/CD workflow for GB Native apps + fn generate_gb_native_workflow(&self, environment: &DeploymentEnvironment) -> String { + let env_name = environment.to_string(); + format!(r#"name: Deploy GB Native App + +on: + push: + branches: [ main, {env_name} ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Build app + run: npm run build + env: + NODE_ENV: production + GB_ENV: {env_name} + + - name: Deploy to GB Platform + run: | + echo "Deploying to GB Platform ({env_name})" + # GB Platform deployment logic here + env: + GB_DEPLOYMENT_TOKEN: ${{{{ secrets.GB_DEPLOYMENT_TOKEN }}}} +"#) + } + + /// Generate CI/CD workflow for Custom apps + fn generate_custom_workflow(&self, framework: &str, node_version: &str, + build_command: &str, output_dir: &str, environment: &DeploymentEnvironment) -> String { + let env_name = environment.to_string(); + format!(r#"name: Deploy Custom {framework} App + +on: + push: + branches: [ main, {env_name} ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '{node_version}' + + - name: Install dependencies + run: npm ci + + - name: Build {framework} app + run: {build_command} + env: + NODE_ENV: production + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: build-output + path: {output_dir} + + - name: Deploy to custom hosting + run: | + echo "Deploying {framework} app to {env_name}" + # Custom deployment logic here +"#) + } +} + impl std::error::Error for ForgejoError {} diff --git a/src/deployment/handlers.rs b/src/deployment/handlers.rs new file mode 100644 index 000000000..8bfdcb049 --- /dev/null +++ b/src/deployment/handlers.rs @@ -0,0 +1,175 @@ +//! API handlers for VibeCode deployment module - Phase 2.5 + +use axum::{ + extract::State, + Json, +}; +use std::sync::Arc; + +use crate::core::shared::state::AppState; + +use super::types::*; +use super::router::DeploymentRouter; + + +/// Configure deployment routes +pub fn configure_deployment_routes() -> axum::Router> { + axum::Router::new() + .route("/api/deployment/types", axum::routing::get(get_app_types)) + .route("/api/deployment/deploy", axum::routing::post(deploy_app)) +} + +/// Get available app types +pub async fn get_app_types( + State(_state): State>, +) -> Result, DeploymentApiError> { + let app_types = vec![ + AppTypeInfo { + id: "gb-native".to_string(), + name: "GB Native".to_string(), + description: "Optimized for General Bots platform with shared resources".to_string(), + features: vec![ + "Shared database connection pool".to_string(), + "Integrated GB authentication".to_string(), + "Shared caching layer".to_string(), + "Auto-scaling".to_string(), + "Built-in monitoring".to_string(), + "Zero configuration".to_string(), + ], + }, + AppTypeInfo { + id: "custom-htmx".to_string(), + name: "Custom HTMX".to_string(), + description: "HTMX-based application with custom deployment".to_string(), + features: vec![ + "Lightweight frontend".to_string(), + "Server-side rendering".to_string(), + "Custom CI/CD pipeline".to_string(), + "Independent deployment".to_string(), + ], + }, + AppTypeInfo { + id: "custom-react".to_string(), + name: "Custom React".to_string(), + description: "React application with custom deployment".to_string(), + features: vec![ + "Modern React".to_string(), + "Vite build system".to_string(), + "Custom CI/CD pipeline".to_string(), + "Independent deployment".to_string(), + ], + }, + AppTypeInfo { + id: "custom-vue".to_string(), + name: "Custom Vue".to_string(), + description: "Vue.js application with custom deployment".to_string(), + features: vec![ + "Vue 3 composition API".to_string(), + "Vite build system".to_string(), + "Custom CI/CD pipeline".to_string(), + "Independent deployment".to_string(), + ], + }, + ]; + + Ok(Json(AppTypesResponse { app_types })) +} + +/// Deploy an application to Forgejo +pub async fn deploy_app( + State(_state): State>, + Json(request): Json, +) -> Result, DeploymentApiError> { + log::info!( + "Deployment request: org={:?}, app={}, type={}, env={}", + request.organization, + request.app_name, + request.app_type, + request.environment + ); + + // Parse app type + let app_type = match request.app_type.as_str() { + "gb-native" => AppType::GbNative { + shared_database: request.shared_database.unwrap_or(true), + shared_auth: request.shared_auth.unwrap_or(true), + shared_cache: request.shared_cache.unwrap_or(true), + }, + custom_type if custom_type.starts_with("custom-") => { + let framework = request.framework.clone() + .unwrap_or_else(|| custom_type.strip_prefix("custom-").unwrap_or("unknown").to_string()); + + AppType::Custom { + framework, + node_version: Some("20".to_string()), + build_command: Some("npm run build".to_string()), + output_directory: Some("dist".to_string()), + } + } + _ => { + return Err(DeploymentApiError::ValidationError(format!( + "Unknown app type: {}", + request.app_type + ))); + } + }; + + // Parse environment + let environment = match request.environment.as_str() { + "development" => DeploymentEnvironment::Development, + "staging" => DeploymentEnvironment::Staging, + "production" => DeploymentEnvironment::Production, + _ => DeploymentEnvironment::Development, + }; + + // Get Forgejo configuration + let forgejo_url = std::env::var("FORGEJO_URL") + .unwrap_or_else(|_| "https://alm.pragmatismo.com.br".to_string()); + + let forgejo_token = std::env::var("FORGEJO_TOKEN").ok(); + + // Get or default organization + let organization = request.organization + .or_else(|| std::env::var("FORGEJO_DEFAULT_ORG").ok()) + .unwrap_or_else(|| "generalbots".to_string()); + + // Create deployment configuration + let config = DeploymentConfig { + organization, + app_name: request.app_name.clone(), + app_type, + environment, + custom_domain: request.custom_domain, + ci_cd_enabled: request.ci_cd_enabled.unwrap_or(true), + }; + + // Create deployment router + let router = DeploymentRouter::new(forgejo_url, forgejo_token); + + // Create placeholder generated app + // In real implementation, this would come from the orchestrator + let generated_app = GeneratedApp::new( + config.app_name.clone(), + format!("Generated {} application", config.app_type), + ); + + // Execute deployment + let result = router.deploy(config, generated_app).await + .map_err(|e| DeploymentApiError::DeploymentFailed(e.to_string()))?; + + log::info!( + "Deployment successful: url={}, repo={}, status={:?}", + result.url, + result.repository, + result.status + ); + + Ok(Json(DeploymentResponse { + success: true, + url: Some(result.url), + repository: Some(result.repository), + app_type: Some(result.app_type), + status: Some(format!("{:?}", result.status)), + error: None, + })) +} diff --git a/src/deployment/mod.rs b/src/deployment/mod.rs index 219a2cdd8..2635f2b48 100644 --- a/src/deployment/mod.rs +++ b/src/deployment/mod.rs @@ -1,434 +1,42 @@ +//! Deployment module for VibeCode platform - Phase 2.5 +//! +//! All apps are deployed to Forgejo repositories using org/app_name format. +//! Two app types: GB Native (optimized for GB platform) and Custom (any framework). +//! +//! # Architecture +//! +//! - `types` - Type definitions for deployment configuration and results +//! - `router` - Deployment router that manages the deployment process +//! - `handlers` - HTTP API handlers for deployment endpoints +//! - `forgejo` - Forgejo client for repository management + +pub mod types; +pub mod router; +pub mod handlers; pub mod forgejo; -use axum::{ - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, - Json, +// Re-export commonly used types from types module +pub use types::{ + AppType, + DeploymentConfig, + DeploymentEnvironment, + DeploymentResult, + DeploymentStatus, + DeploymentError, + GeneratedApp, + GeneratedFile, + DeploymentRequest, + DeploymentResponse, + AppTypesResponse, + AppTypeInfo, + DeploymentApiError, }; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::sync::Arc; -use crate::core::shared::state::AppState; +// Re-export deployment router +pub use router::DeploymentRouter; -// Re-export types from forgejo module -pub use forgejo::{AppType, BuildConfig, ForgejoClient, ForgejoError, ForgejoRepo}; +// Re-export route configuration function +pub use handlers::configure_deployment_routes; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DeploymentTarget { - /// Serve from GB platform (/apps/{name}) - Internal { - route: String, - shared_resources: bool, - }, - /// Deploy to external Forgejo repository - External { - repo_url: String, - custom_domain: Option, - ci_cd_enabled: bool, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeploymentConfig { - pub app_name: String, - pub target: DeploymentTarget, - pub environment: DeploymentEnvironment, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DeploymentEnvironment { - Development, - Staging, - Production, -} - -pub struct DeploymentRouter { - forgejo_url: String, - forgejo_token: Option, - internal_base_path: PathBuf, -} - -impl DeploymentRouter { - pub fn new( - forgejo_url: String, - forgejo_token: Option, - internal_base_path: PathBuf, - ) -> Self { - Self { - forgejo_url, - forgejo_token, - internal_base_path, - } - } - - /// Route deployment based on target type - pub async fn deploy( - &self, - config: DeploymentConfig, - generated_app: GeneratedApp, - ) -> Result { - match config.target { - DeploymentTarget::Internal { route, .. } => { - self.deploy_internal(route, generated_app).await - } - DeploymentTarget::External { ref repo_url, .. } => { - self.deploy_external(repo_url, generated_app).await - } - } - } - - /// Deploy internally to GB platform - async fn deploy_internal( - &self, - route: String, - app: GeneratedApp, - ) -> Result { - // 1. Store files in Drive - // 2. Register route in app router - // 3. Create API endpoints - // 4. Return deployment URL - - let url = format!("/apps/{}/", route); - - Ok(DeploymentResult { - url, - deployment_type: "internal".to_string(), - status: DeploymentStatus::Deployed, - metadata: serde_json::json!({ - "route": route, - "platform": "gb", - }), - }) - } - - /// Deploy externally to Forgejo - async fn deploy_external( - &self, - repo_url: &str, - app: GeneratedApp, - ) -> Result { - // 1. Initialize git repo - // 2. Add Forgejo remote - // 3. Push generated files - // 4. Create CI/CD workflow - // 5. Trigger build - - Ok(DeploymentResult { - url: repo_url.to_string(), - deployment_type: "external".to_string(), - status: DeploymentStatus::Pending, - metadata: serde_json::json!({ - "repo_url": repo_url, - "forgejo": self.forgejo_url, - }), - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeploymentResult { - pub url: String, - pub deployment_type: String, - pub status: DeploymentStatus, - pub metadata: serde_json::Value, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DeploymentStatus { - Pending, - Building, - Deployed, - Failed, -} - -#[derive(Debug)] -pub enum DeploymentError { - InternalDeploymentError(String), - ForgejoError(String), - GitError(String), - CiCdError(String), -} - -impl std::fmt::Display for DeploymentError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DeploymentError::InternalDeploymentError(msg) => { - write!(f, "Internal deployment error: {}", msg) - } - DeploymentError::ForgejoError(msg) => write!(f, "Forgejo error: {}", msg), - DeploymentError::GitError(msg) => write!(f, "Git error: {}", msg), - DeploymentError::CiCdError(msg) => write!(f, "CI/CD error: {}", msg), - } - } -} - -impl std::error::Error for DeploymentError {} - -impl From for DeploymentError { - fn from(err: ForgejoError) -> Self { - DeploymentError::ForgejoError(err.to_string()) - } -} - -#[derive(Debug, Clone)] -pub struct GeneratedApp { - pub name: String, - pub description: String, - pub files: Vec, -} - -#[derive(Debug, Clone)] -pub struct GeneratedFile { - pub path: String, - pub content: Vec, -} - -impl GeneratedApp { - pub fn temp_dir(&self) -> Result { - let temp_dir = std::env::temp_dir() - .join("gb-deployments") - .join(&self.name); - Ok(temp_dir) - } - - pub fn new(name: String, description: String) -> Self { - Self { - name, - description, - files: Vec::new(), - } - } - - pub fn add_file(&mut self, path: String, content: Vec) { - self.files.push(GeneratedFile { path, content }); - } - - pub fn add_text_file(&mut self, path: String, content: String) { - self.add_file(path, content.into_bytes()); - } -} - -// ============================================================================= -// API Types and Handlers -// ============================================================================= - -#[derive(Debug, Deserialize)] -pub struct DeploymentRequest { - pub app_name: String, - pub target: String, - pub environment: String, - pub manifest: serde_json::Value, -} - -#[derive(Debug, Serialize)] -pub struct DeploymentResponse { - pub success: bool, - pub url: Option, - pub deployment_type: Option, - pub status: Option, - pub error: Option, -} - -#[derive(Debug, Serialize)] -pub struct DeploymentTargetsResponse { - pub targets: Vec, -} - -#[derive(Debug, Serialize)] -pub struct DeploymentTargetInfo { - pub id: String, - pub name: String, - pub description: String, - pub features: Vec, -} - -/// Configure deployment routes -pub fn configure_deployment_routes() -> axum::Router> { - axum::Router::new() - .route("/api/deployment/targets", axum::routing::get(get_deployment_targets)) - .route("/api/deployment/deploy", axum::routing::post(deploy_app)) -} - -/// Get available deployment targets -pub async fn get_deployment_targets( - State(_state): State>, -) -> Result, DeploymentApiError> { - let targets = vec![ - DeploymentTargetInfo { - id: "internal".to_string(), - name: "GB Platform".to_string(), - description: "Deploy internally to General Bots platform".to_string(), - features: vec![ - "Instant deployment".to_string(), - "Shared resources".to_string(), - "Auto-scaling".to_string(), - "Built-in monitoring".to_string(), - "Zero configuration".to_string(), - ], - }, - DeploymentTargetInfo { - id: "external".to_string(), - name: "Forgejo ALM".to_string(), - description: "Deploy to external Git repository with CI/CD".to_string(), - features: vec![ - "Git-based deployment".to_string(), - "Custom domains".to_string(), - "CI/CD pipelines".to_string(), - "Version control".to_string(), - "Team collaboration".to_string(), - ], - }, - ]; - - Ok(Json(DeploymentTargetsResponse { targets })) -} - -/// Deploy an application -pub async fn deploy_app( - State(state): State>, - Json(request): Json, -) -> Result, DeploymentApiError> { - log::info!( - "Deployment request received: app={}, target={}, env={}", - request.app_name, - request.target, - request.environment - ); - - // Parse deployment target - let target = match request.target.as_str() { - "internal" => { - let route = request.manifest - .get("route") - .and_then(|v| v.as_str()) - .unwrap_or(&request.app_name) - .to_string(); - - let shared_resources = request.manifest - .get("shared_resources") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - DeploymentTarget::Internal { - route, - shared_resources, - } - } - "external" => { - let repo_url = request.manifest - .get("repo_url") - .and_then(|v| v.as_str()) - .ok_or_else(|| DeploymentApiError::ValidationError("repo_url is required for external deployment".to_string()))? - .to_string(); - - let custom_domain = request.manifest - .get("custom_domain") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let ci_cd_enabled = request.manifest - .get("ci_cd_enabled") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - DeploymentTarget::External { - repo_url, - custom_domain, - ci_cd_enabled, - } - } - _ => { - return Err(DeploymentApiError::ValidationError(format!( - "Unknown deployment target: {}", - request.target - ))); - } - }; - - // Parse environment - let environment = match request.environment.as_str() { - "development" => DeploymentEnvironment::Development, - "staging" => DeploymentEnvironment::Staging, - "production" => DeploymentEnvironment::Production, - _ => DeploymentEnvironment::Development, - }; - - // Create deployment configuration - let config = DeploymentConfig { - app_name: request.app_name.clone(), - target, - environment, - }; - - // Get Forgejo configuration from environment - let forgejo_url = std::env::var("FORGEJO_URL") - .unwrap_or_else(|_| "https://alm.pragmatismo.com.br".to_string()); - - let forgejo_token = std::env::var("FORGEJO_TOKEN").ok(); - - // Create deployment router - let internal_base_path = std::path::PathBuf::from("/opt/gbo/data/apps"); - let router = DeploymentRouter::new(forgejo_url, forgejo_token, internal_base_path); - - // Create a placeholder generated app - // In real implementation, this would come from the orchestrator - let generated_app = GeneratedApp::new( - config.app_name.clone(), - "Generated application".to_string(), - ); - - // Execute deployment - let result = router.deploy(config, generated_app).await - .map_err(|e| DeploymentApiError::DeploymentFailed(e.to_string()))?; - - log::info!( - "Deployment successful: url={}, type={}, status={:?}", - result.url, - result.deployment_type, - result.status - ); - - Ok(Json(DeploymentResponse { - success: true, - url: Some(result.url), - deployment_type: Some(result.deployment_type), - status: Some(format!("{:?}", result.status)), - error: None, - })) -} - -#[derive(Debug)] -pub enum DeploymentApiError { - ValidationError(String), - DeploymentFailed(String), - InternalError(String), -} - -impl IntoResponse for DeploymentApiError { - fn into_response(self) -> Response { - use crate::security::error_sanitizer::log_and_sanitize; - - let (status, message) = match self { - DeploymentApiError::ValidationError(msg) => { - (StatusCode::BAD_REQUEST, msg) - } - DeploymentApiError::DeploymentFailed(msg) => { - let sanitized = log_and_sanitize(&msg, "deployment", None); - (StatusCode::INTERNAL_SERVER_ERROR, sanitized) - } - DeploymentApiError::InternalError(msg) => { - let sanitized = log_and_sanitize(&msg, "deployment", None); - (StatusCode::INTERNAL_SERVER_ERROR, sanitized) - } - }; - - let body = Json(serde_json::json!({ - "success": false, - "error": message, - })); - - (status, body).into_response() - } -} +// Re-export Forgejo types +pub use forgejo::{ForgejoClient, ForgejoError, ForgejoRepo}; diff --git a/src/deployment/router.rs b/src/deployment/router.rs new file mode 100644 index 000000000..54f3eb6b8 --- /dev/null +++ b/src/deployment/router.rs @@ -0,0 +1,102 @@ +//! Deployment router for VibeCode platform - Phase 2.5 +//! +//! All apps are deployed to Forgejo repositories using org/app_name format. + +use super::types::*; +use super::forgejo::ForgejoClient; + +/// Deployment router - all apps go to Forgejo +pub struct DeploymentRouter { + forgejo_url: String, + forgejo_token: Option, +} + +impl DeploymentRouter { + pub fn new(forgejo_url: String, forgejo_token: Option) -> Self { + Self { + forgejo_url, + forgejo_token, + } + } + + /// Deploy to Forgejo repository (org/app_name) + pub async fn deploy( + &self, + config: DeploymentConfig, + generated_app: GeneratedApp, + ) -> Result { + log::info!( + "Deploying {} app: {}/{} to {} environment", + config.app_type, + config.organization, + config.app_name, + config.environment + ); + + // Get or create Forgejo client + let token = self.forgejo_token.clone() + .ok_or_else(|| DeploymentError::ConfigurationError("FORGEJO_TOKEN not configured".to_string()))?; + + let client = ForgejoClient::new(self.forgejo_url.clone(), token); + + // Create repository if it doesn't exist + let repo = client.create_repository( + &config.app_name, + &generated_app.description, + false, // public repo + ).await?; + + log::info!("Repository created/verified: {}", repo.clone_url); + + // Push app to repository + let branch = config.environment.to_string(); + client.push_app(&repo.clone_url, &generated_app, &branch).await?; + + // Create CI/CD workflow if enabled + if config.ci_cd_enabled { + client.create_cicd_workflow(&repo.clone_url, &config.app_type, &config.environment).await?; + log::info!("CI/CD workflow created for {}/{}", config.organization, config.app_name); + } + + // Build deployment URL + let url = self.build_deployment_url(&config); + + Ok(DeploymentResult { + url, + repository: repo.clone_url, + app_type: config.app_type.to_string(), + environment: config.environment.to_string(), + status: if config.ci_cd_enabled { + DeploymentStatus::Building + } else { + DeploymentStatus::Deployed + }, + metadata: serde_json::json!({ + "org": config.organization, + "app_name": config.app_name, + "repo_id": repo.id, + "forgejo_url": self.forgejo_url, + "custom_domain": config.custom_domain, + }), + }) + } + + /// Build deployment URL based on environment and domain + fn build_deployment_url(&self, config: &DeploymentConfig) -> String { + if let Some(ref domain) = config.custom_domain { + format!("https://{}/", domain) + } else { + match config.environment { + DeploymentEnvironment::Production => { + format!("https://{}.gb.solutions/", config.app_name) + } + DeploymentEnvironment::Staging => { + format!("https://{}-staging.gb.solutions/", config.app_name) + } + DeploymentEnvironment::Development => { + format!("https://{}-dev.gb.solutions/", config.app_name) + } + } + } + } +} diff --git a/src/deployment/types.rs b/src/deployment/types.rs new file mode 100644 index 000000000..c3a91522f --- /dev/null +++ b/src/deployment/types.rs @@ -0,0 +1,270 @@ +//! Type definitions for VibeCode deployment module - Phase 2.5 + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +// Re-export ForgejoError for From implementation +use super::forgejo::ForgejoError; + +/// App type determines the deployment strategy and CI/CD workflow +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum AppType { + /// GB Native - Optimized for General Bots platform + /// Uses GB-specific features, shared resources, and optimized runtime + GbNative { + /// Use GB shared database connection pool + shared_database: bool, + /// Use GB authentication system + shared_auth: bool, + /// Use GB caching layer + shared_cache: bool, + }, + /// Custom - Any framework or technology + /// Fully independent deployment with custom CI/CD + Custom { + /// Framework type: htmx, react, vue, nextjs, svelte, etc. + framework: String, + /// Node.js version for build + node_version: Option, + /// Build command + build_command: Option, + /// Output directory + output_directory: Option, + }, +} + +impl Default for AppType { + fn default() -> Self { + AppType::GbNative { + shared_database: true, + shared_auth: true, + shared_cache: true, + } + } +} + +impl std::fmt::Display for AppType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppType::GbNative { .. } => write!(f, "gb-native"), + AppType::Custom { framework, .. } => write!(f, "custom-{}", framework), + } + } +} + +/// Deployment configuration for all apps +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeploymentConfig { + /// Organization name (becomes part of repo: org/app_name) + pub organization: String, + /// Application name (becomes part of repo: org/app_name) + pub app_name: String, + /// App type determines deployment strategy + pub app_type: AppType, + /// Deployment environment + pub environment: DeploymentEnvironment, + /// Custom domain (optional) + pub custom_domain: Option, + /// Enable CI/CD pipeline + pub ci_cd_enabled: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum DeploymentEnvironment { + Development, + Staging, + Production, +} + +impl Default for DeploymentEnvironment { + fn default() -> Self { + DeploymentEnvironment::Development + } +} + +impl std::fmt::Display for DeploymentEnvironment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeploymentEnvironment::Development => write!(f, "development"), + DeploymentEnvironment::Staging => write!(f, "staging"), + DeploymentEnvironment::Production => write!(f, "production"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeploymentResult { + pub url: String, + pub repository: String, + pub app_type: String, + pub environment: String, + pub status: DeploymentStatus, + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum DeploymentStatus { + Pending, + Building, + Deployed, + Failed, +} + +#[derive(Debug)] +pub enum DeploymentError { + ConfigurationError(String), + ForgejoError(String), + GitError(String), + CiCdError(String), +} + +impl std::fmt::Display for DeploymentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeploymentError::ConfigurationError(msg) => { + write!(f, "Configuration error: {}", msg) + } + DeploymentError::ForgejoError(msg) => write!(f, "Forgejo error: {}", msg), + DeploymentError::GitError(msg) => write!(f, "Git error: {}", msg), + DeploymentError::CiCdError(msg) => write!(f, "CI/CD error: {}", msg), + } + } +} + +impl std::error::Error for DeploymentError {} + +impl From for DeploymentError { + fn from(err: ForgejoError) -> Self { + DeploymentError::ForgejoError(err.to_string()) + } +} + +/// Helper type for wrapping string errors to implement Error trait +#[derive(Debug)] +struct StringError(String); + +impl fmt::Display for StringError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for StringError {} + +#[derive(Debug, Clone)] +pub struct GeneratedApp { + pub name: String, + pub description: String, + pub files: Vec, +} + +#[derive(Debug, Clone)] +pub struct GeneratedFile { + pub path: String, + pub content: Vec, +} + +impl GeneratedApp { + pub fn new(name: String, description: String) -> Self { + Self { + name, + description, + files: Vec::new(), + } + } + + pub fn temp_dir(&self) -> Result { + let temp_dir = std::env::temp_dir() + .join("gb-deployments") + .join(&self.name); + Ok(temp_dir) + } + + pub fn add_file(&mut self, path: String, content: Vec) { + self.files.push(GeneratedFile { path, content }); + } + + pub fn add_text_file(&mut self, path: String, content: String) { + self.add_file(path, content.into_bytes()); + } +} + +// ============================================================================= +// API Types +// ============================================================================= + +#[derive(Debug, Deserialize)] +pub struct DeploymentRequest { + pub organization: Option, + pub app_name: String, + pub app_type: String, + pub framework: Option, + pub environment: String, + pub custom_domain: Option, + pub ci_cd_enabled: Option, + pub shared_database: Option, + pub shared_auth: Option, + pub shared_cache: Option, +} + +#[derive(Debug, Serialize)] +pub struct DeploymentResponse { + pub success: bool, + pub url: Option, + pub repository: Option, + pub app_type: Option, + pub status: Option, + pub error: Option, +} + +#[derive(Debug, Serialize)] +pub struct AppTypesResponse { + pub app_types: Vec, +} + +#[derive(Debug, Serialize)] +pub struct AppTypeInfo { + pub id: String, + pub name: String, + pub description: String, + pub features: Vec, +} + +#[derive(Debug)] +pub enum DeploymentApiError { + ValidationError(String), + DeploymentFailed(String), + ConfigurationError(String), +} + +impl IntoResponse for DeploymentApiError { + fn into_response(self) -> Response { + use crate::security::error_sanitizer::log_and_sanitize; + + let (status, message) = match self { + DeploymentApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg), + DeploymentApiError::DeploymentFailed(msg) => { + let error = StringError(msg); + let sanitized = log_and_sanitize(&error, "deployment", None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized.message) + } + DeploymentApiError::ConfigurationError(msg) => { + let error = StringError(msg); + let sanitized = log_and_sanitize(&error, "deployment_config", None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized.message) + } + }; + + let body = Json(serde_json::json!({ + "success": false, + "error": message, + })); + + (status, body).into_response() + } +}