Fix deployment module: add types, router, handlers and fix forgejo integration
All checks were successful
BotServer CI / build (push) Successful in 10m21s
All checks were successful
BotServer CI / build (push) Successful in 10m21s
This commit is contained in:
parent
6195062482
commit
7a22798c23
6 changed files with 684 additions and 555 deletions
|
|
@ -111,6 +111,7 @@ dirs = { workspace = true }
|
||||||
dotenvy = { workspace = true }
|
dotenvy = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
|
git2 = "0.19"
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
hmac = { workspace = true }
|
hmac = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use git2::{Repository, Oid, Signature, Time};
|
use git2::{Repository, Signature};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
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 {
|
pub struct ForgejoClient {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
|
|
@ -129,17 +130,20 @@ impl ForgejoClient {
|
||||||
Ok(oid.to_string())
|
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(
|
pub async fn create_cicd_workflow(
|
||||||
&self,
|
&self,
|
||||||
repo_url: &str,
|
repo_url: &str,
|
||||||
app_type: AppType,
|
app_type: &AppType,
|
||||||
build_config: BuildConfig,
|
environment: &DeploymentEnvironment,
|
||||||
) -> Result<(), DeploymentError> {
|
) -> Result<(), DeploymentError> {
|
||||||
let workflow = match app_type {
|
let workflow = match app_type {
|
||||||
AppType::Htmx => self.generate_htmx_workflow(build_config),
|
AppType::GbNative { .. } => self.generate_gb_native_workflow(environment),
|
||||||
AppType::React => self.generate_react_workflow(build_config),
|
AppType::Custom { framework, node_version, build_command, output_directory } => {
|
||||||
AppType::Vue => self.generate_vue_workflow(build_config),
|
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
|
// 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -318,29 +227,7 @@ struct CreateRepoRequest {
|
||||||
readme: Option<String>,
|
readme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
// AppType and related types are now defined in types.rs
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ForgejoError {
|
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 {}
|
impl std::error::Error for ForgejoError {}
|
||||||
|
|
|
||||||
175
src/deployment/handlers.rs
Normal file
175
src/deployment/handlers.rs
Normal file
|
|
@ -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<Arc<AppState>> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
) -> Result<Json<AppTypesResponse>, 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<Arc<AppState>>,
|
||||||
|
Json(request): Json<DeploymentRequest>,
|
||||||
|
) -> Result<Json<DeploymentResponse>, 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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
@ -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;
|
pub mod forgejo;
|
||||||
|
|
||||||
use axum::{
|
// Re-export commonly used types from types module
|
||||||
extract::State,
|
pub use types::{
|
||||||
http::StatusCode,
|
AppType,
|
||||||
response::{IntoResponse, Response},
|
DeploymentConfig,
|
||||||
Json,
|
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
|
// Re-export route configuration function
|
||||||
pub use forgejo::{AppType, BuildConfig, ForgejoClient, ForgejoError, ForgejoRepo};
|
pub use handlers::configure_deployment_routes;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
// Re-export Forgejo types
|
||||||
pub enum DeploymentTarget {
|
pub use forgejo::{ForgejoClient, ForgejoError, ForgejoRepo};
|
||||||
/// Serve from GB platform (/apps/{name})
|
|
||||||
Internal {
|
|
||||||
route: String,
|
|
||||||
shared_resources: bool,
|
|
||||||
},
|
|
||||||
/// Deploy to external Forgejo repository
|
|
||||||
External {
|
|
||||||
repo_url: String,
|
|
||||||
custom_domain: Option<String>,
|
|
||||||
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<String>,
|
|
||||||
internal_base_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeploymentRouter {
|
|
||||||
pub fn new(
|
|
||||||
forgejo_url: String,
|
|
||||||
forgejo_token: Option<String>,
|
|
||||||
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<DeploymentResult, DeploymentError> {
|
|
||||||
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<DeploymentResult, DeploymentError> {
|
|
||||||
// 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<DeploymentResult, DeploymentError> {
|
|
||||||
// 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<ForgejoError> 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<GeneratedFile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct GeneratedFile {
|
|
||||||
pub path: String,
|
|
||||||
pub content: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GeneratedApp {
|
|
||||||
pub fn temp_dir(&self) -> Result<PathBuf, DeploymentError> {
|
|
||||||
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<u8>) {
|
|
||||||
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<String>,
|
|
||||||
pub deployment_type: Option<String>,
|
|
||||||
pub status: Option<String>,
|
|
||||||
pub error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct DeploymentTargetsResponse {
|
|
||||||
pub targets: Vec<DeploymentTargetInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct DeploymentTargetInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub description: String,
|
|
||||||
pub features: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configure deployment routes
|
|
||||||
pub fn configure_deployment_routes() -> axum::Router<Arc<AppState>> {
|
|
||||||
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<Arc<AppState>>,
|
|
||||||
) -> Result<Json<DeploymentTargetsResponse>, 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<Arc<AppState>>,
|
|
||||||
Json(request): Json<DeploymentRequest>,
|
|
||||||
) -> Result<Json<DeploymentResponse>, 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
102
src/deployment/router.rs
Normal file
102
src/deployment/router.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeploymentRouter {
|
||||||
|
pub fn new(forgejo_url: String, forgejo_token: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
forgejo_url,
|
||||||
|
forgejo_token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deploy to Forgejo repository (org/app_name)
|
||||||
|
pub async fn deploy(
|
||||||
|
&self,
|
||||||
|
config: DeploymentConfig,
|
||||||
|
generated_app: GeneratedApp,
|
||||||
|
) -> Result<DeploymentResult, DeploymentError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
270
src/deployment/types.rs
Normal file
270
src/deployment/types.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
/// Build command
|
||||||
|
build_command: Option<String>,
|
||||||
|
/// Output directory
|
||||||
|
output_directory: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
/// 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<ForgejoError> 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<GeneratedFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GeneratedFile {
|
||||||
|
pub path: String,
|
||||||
|
pub content: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GeneratedApp {
|
||||||
|
pub fn new(name: String, description: String) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
files: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp_dir(&self) -> Result<std::path::PathBuf, DeploymentError> {
|
||||||
|
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<u8>) {
|
||||||
|
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<String>,
|
||||||
|
pub app_name: String,
|
||||||
|
pub app_type: String,
|
||||||
|
pub framework: Option<String>,
|
||||||
|
pub environment: String,
|
||||||
|
pub custom_domain: Option<String>,
|
||||||
|
pub ci_cd_enabled: Option<bool>,
|
||||||
|
pub shared_database: Option<bool>,
|
||||||
|
pub shared_auth: Option<bool>,
|
||||||
|
pub shared_cache: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DeploymentResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub repository: Option<String>,
|
||||||
|
pub app_type: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct AppTypesResponse {
|
||||||
|
pub app_types: Vec<AppTypeInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct AppTypeInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue