Fix deployment module: add types, router, handlers and fix forgejo integration
All checks were successful
BotServer CI / build (push) Successful in 10m21s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-03 14:30:44 -03:00
parent 6195062482
commit 7a22798c23
6 changed files with 684 additions and 555 deletions

View file

@ -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 }

View file

@ -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<String>,
}
#[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 {}

175
src/deployment/handlers.rs Normal file
View 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,
}))
}

View file

@ -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,
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use crate::core::shared::state::AppState;
// Re-export types from forgejo module
pub use forgejo::{AppType, BuildConfig, ForgejoClient, ForgejoError, ForgejoRepo};
#[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<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
)));
}
// Re-export commonly used types from types module
pub use types::{
AppType,
DeploymentConfig,
DeploymentEnvironment,
DeploymentResult,
DeploymentStatus,
DeploymentError,
GeneratedApp,
GeneratedFile,
DeploymentRequest,
DeploymentResponse,
AppTypesResponse,
AppTypeInfo,
DeploymentApiError,
};
// Parse environment
let environment = match request.environment.as_str() {
"development" => DeploymentEnvironment::Development,
"staging" => DeploymentEnvironment::Staging,
"production" => DeploymentEnvironment::Production,
_ => DeploymentEnvironment::Development,
};
// Re-export deployment router
pub use router::DeploymentRouter;
// Create deployment configuration
let config = DeploymentConfig {
app_name: request.app_name.clone(),
target,
environment,
};
// Re-export route configuration function
pub use handlers::configure_deployment_routes;
// 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};

102
src/deployment/router.rs Normal file
View 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
View 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()
}
}