diff --git a/.gitignore b/.gitignore index db6af84..cb1013b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,30 @@ +``` # Dependencies node_modules/ target/ -_build/ -deps/ -envs/prod.env -envs/staging.env -envs/env.env -# Build outputs -dist/ -build/ -out/ -.next/ -.docusaurus/ -# Environment files -.env* -!.env.example - -# IDE -.vscode/ -.idea/ -*.iml -.swp -.swo -.sublime-* - -# OS -.DS_Store -Thumbs.db -.AppleDouble -.LSOverride +# Environment +.env +.env.local +.env.* # Logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -# Cache -.cache/ -.pnpm-store/ -.turbo/ -.vercel/ -*.tsbuildinfo - -# Test artifacts -test-results/ -playwright-report/ -.coverage -htmlcov/ +# Coverage coverage/ +htmlcov/ +.coverage -# Python -__pycache__/ -*.py[cod] -*.pyc -.pytest_cache/ -.mypy_cache/ -.ruff_cache/ -*.egg-info/ -.venv/ -venv/ -env/ - -# Rust -**/*.rs.bk -*.pdb - -# Elixir/Phoenix -*.ez -erl_crash.dump -.fetch - -# Build artifacts -*.o -*.so -*.dylib -*.dll -*.exe - -# Database -*.sqlite -*.db -*.sqlite3 +# Temporary files +*.tmp +*.swp +*.swo -# Docker -docker-compose.override.yml +# OS generated files +.DS_Store +Thumbs.db +# Rust specific +Cargo.lock +``` \ No newline at end of file diff --git a/README.md b/README.md index ee2948c..c4f4400 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,33 @@ The config drives: - **SEO**: title, description, keywords, favicon, OG image - **Branding**: logo path and alt text - **Repo**: GitHub org/repo for links and edit URLs -- **Deployment**: `appName` for `/opt//{frontend,backend}/{staging,production}`, systemd service names (staging/production) +- **Deployment**: `appName` for `/opt//{frontend,backend}/{staging,production}`, systemd service names (staging/production), dedicated port mappings See `template.config.example.yaml` for every option. After applying, commit the changes and set up GitHub Environments and secrets as in `infra/systemd/README.md`. +## 🔍 Finding Available Ports + +Before deploying a new app, use the port finder script to scan running services and get port recommendations: + +```bash +# Scan all services and get recommendations +pnpm run find-available-port + +# Or run directly +./scripts/find-available-port.sh + +# Filter by app name +./scripts/find-available-port.sh myapp + +# Filter by environment +./scripts/find-available-port.sh myapp staging + +# Filter by component +./scripts/find-available-port.sh myapp production backend +``` + +The script scans `/etc/systemd/system` and `/lib/systemd/system` for services matching the `{appname}-{env}-{frontend|backend}.service` pattern, extracts configured ports, checks which are actively listening, and recommends available ports in the appropriate ranges. + ## 🚀 Features - **Backend**: Rust-based API server with base endpoints diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs index e01844a..b5685b8 100644 --- a/apps/backend/src/main.rs +++ b/apps/backend/src/main.rs @@ -5,6 +5,7 @@ use axum::{ Router, }; use serde::{Deserialize, Serialize}; +use std::env; use std::net::SocketAddr; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; @@ -41,6 +42,14 @@ async fn main() { .with_env_filter("backend=debug,tower_http=debug") .init(); + // Get port from command line args or environment variable (default: 3000) + let port: u16 = std::env::args() + .nth(1) + .and_then(|arg| arg.strip_prefix("--port=").map(|s| s.to_string())) + .or_else(|| env::var("PORT").ok()) + .and_then(|s| s.parse().ok()) + .unwrap_or(3000); + // Build application with routes let app = Router::new() .route("/", get(root)) @@ -51,7 +60,7 @@ async fn main() { .layer(TraceLayer::new_for_http()); // Run server - let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); + let addr = SocketAddr::from(([0, 0, 0, 0], port)); tracing::info!("🚀 Server starting on http://{}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); diff --git a/infra/systemd/README.md b/infra/systemd/README.md index fe7ce38..b16faa5 100644 --- a/infra/systemd/README.md +++ b/infra/systemd/README.md @@ -2,10 +2,59 @@ Each app is deployed in **two environments** on the VM: **staging** and **production**. Paths and service names are env-specific. CI/CD uses the same **env mode** mapping for all workflows; see [.github/ENV_MODES.md](../../.github/ENV_MODES.md). +## Naming Convention + +Service files follow the naming pattern: **`{appname}-{env}-{frontend|backend}.service`** + +Examples: +- `myapp-staging-frontend.service` +- `myapp-staging-backend.service` +- `myapp-production-frontend.service` +- `myapp-production-backend.service` + +This convention ensures consistent identification of services across multiple apps on the same VM. + +## Port Allocation + +Each service has a **dedicated port** configured via environment variables: + +| Component | Environment | Default Port Range | Example Port | +|-----------|-------------|-------------------|--------------| +| Backend | Staging | 6100-6199 | 6100 | +| Frontend | Staging | 6200-6299 | 6200 | +| Backend | Production | 6300-6399 | 6300 | +| Frontend | Production | 6400-6499 | 6400 | + +Ports are configured in `template.config.yaml` under `deployment:` section and applied via `pnpm run apply-template`. + +### Finding Available Ports + +Use the provided script to scan running services and find available ports: + +```bash +# Scan all services and get recommendations +./scripts/find-available-port.sh + +# Filter by app name +./scripts/find-available-port.sh myapp + +# Filter by environment +./scripts/find-available-port.sh myapp staging + +# Filter by component +./scripts/find-available-port.sh myapp production backend +``` + +The script will: +1. Scan `/etc/systemd/system` and `/lib/systemd/system` for matching service files +2. Extract configured ports from service files +3. Check which ports are actively listening +4. Recommend available ports in the appropriate ranges + | Branch | Environment | Deploy paths (on VM) | Systemd units | |----------|-------------|----------------------|----------------| -| `main` | **staging** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging` | `__TEMPLATE_SYSTEMD_BACKEND_STAGING__`, `__TEMPLATE_SYSTEMD_FRONTEND_STAGING__` | -| `prod` | **production** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production` | `__TEMPLATE_SYSTEMD_BACKEND_PRODUCTION__`, `__TEMPLATE_SYSTEMD_FRONTEND_PRODUCTION__` | +| `main` | **staging** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging` | `{appname}-staging-backend.service`, `{appname}-staging-frontend.service` | +| `prod` | **production** | `/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production`, `/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production` | `{appname}-production-backend.service`, `{appname}-production-frontend.service` | Paths follow `/opt//{frontend,backend}/{staging,production}`. diff --git a/infra/systemd/backend-production.service.example b/infra/systemd/backend-production.service.example index 37d06e9..d2038aa 100644 --- a/infra/systemd/backend-production.service.example +++ b/infra/systemd/backend-production.service.example @@ -1,22 +1,26 @@ # Example systemd unit for backend (production). # IMPORTANT: Create a service file named: __TEMPLATE_BACKEND_PRODUCTION_SERVICE_FILE__ -# (e.g., solo-template-backend-production.service) in infra/systemd/ before deploying. +# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-production-backend.service) # Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_BACKEND_PRODUCTION__.service on the VM. # Path: /opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production/backend # Each app: /opt//{frontend,backend}/{staging,production}. +# Port: __TEMPLATE_BACKEND_PRODUCTION_PORT__ (dedicated port for this app/env combination) [Unit] -Description=__TEMPLATE_APP_NAME__ Backend API (production) +Description=__TEMPLATE_APP_NAME__ Backend API (production) - Port __TEMPLATE_BACKEND_PRODUCTION_PORT__ After=network-online.target Wants=network-online.target [Service] Type=simple -ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production/backend +ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production/backend --port __TEMPLATE_BACKEND_PRODUCTION_PORT__ Restart=on-failure RestartSec=5 WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/production EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_BACKEND_PRODUCTION__ +Environment=PORT=__TEMPLATE_BACKEND_PRODUCTION_PORT__ +Environment=APP_ENV=production +Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__ [Install] WantedBy=multi-user.target diff --git a/infra/systemd/backend-staging.service.example b/infra/systemd/backend-staging.service.example index 275f5b8..1f23364 100644 --- a/infra/systemd/backend-staging.service.example +++ b/infra/systemd/backend-staging.service.example @@ -1,23 +1,27 @@ # Example systemd unit for backend (staging). # IMPORTANT: Create a service file named: __TEMPLATE_BACKEND_STAGING_SERVICE_FILE__ -# (e.g., solo-template-backend-staging.service) in infra/systemd/ before deploying. +# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-staging-backend.service) # The CI/CD workflow expects this file to exist. # Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_BACKEND_STAGING__.service on the VM. # Path: /opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging/backend # Each app: /opt//{frontend,backend}/{staging,production}. +# Port: __TEMPLATE_BACKEND_STAGING_PORT__ (dedicated port for this app/env combination) [Unit] -Description=__TEMPLATE_APP_NAME__ Backend API (staging) +Description=__TEMPLATE_APP_NAME__ Backend API (staging) - Port __TEMPLATE_BACKEND_STAGING_PORT__ After=network-online.target Wants=network-online.target [Service] Type=simple -ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging/backend +ExecStart=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging/backend --port __TEMPLATE_BACKEND_STAGING_PORT__ Restart=on-failure RestartSec=5 WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/backend/staging EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_BACKEND_STAGING__ +Environment=PORT=__TEMPLATE_BACKEND_STAGING_PORT__ +Environment=APP_ENV=staging +Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__ [Install] WantedBy=multi-user.target diff --git a/infra/systemd/frontend-production.service.example b/infra/systemd/frontend-production.service.example index 3cb3c54..cc0925f 100644 --- a/infra/systemd/frontend-production.service.example +++ b/infra/systemd/frontend-production.service.example @@ -1,23 +1,27 @@ # Example systemd unit for frontend (production). # IMPORTANT: Create a service file named: __TEMPLATE_FRONTEND_PRODUCTION_SERVICE_FILE__ -# (e.g., solo-template-frontend-production.service) in infra/systemd/ before deploying. +# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-production-frontend.service) # Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_FRONTEND_PRODUCTION__.service on the VM. # Serves from /opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production (SvelteKit adapter-node SSR build). # Each app: /opt//{frontend,backend}/{staging,production}. +# Port: __TEMPLATE_FRONTEND_PRODUCTION_PORT__ (dedicated port for this app/env combination) [Unit] -Description=__TEMPLATE_APP_NAME__ Frontend (production) +Description=__TEMPLATE_APP_NAME__ Frontend (production) - Port __TEMPLATE_FRONTEND_PRODUCTION_PORT__ After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/production -ExecStart=/usr/bin/node serve.mjs +ExecStart=/usr/bin/node serve.mjs --port __TEMPLATE_FRONTEND_PRODUCTION_PORT__ Restart=on-failure RestartSec=5 EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_FRONTEND_PRODUCTION__ Environment=PORT=__TEMPLATE_FRONTEND_PRODUCTION_PORT__ +Environment=APP_ENV=production +Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__ +Environment=BACKEND_URL=http://localhost:__TEMPLATE_BACKEND_PRODUCTION_PORT__ [Install] WantedBy=multi-user.target diff --git a/infra/systemd/frontend-staging.service.example b/infra/systemd/frontend-staging.service.example index 7d4faed..14994da 100644 --- a/infra/systemd/frontend-staging.service.example +++ b/infra/systemd/frontend-staging.service.example @@ -1,24 +1,28 @@ # Example systemd unit for frontend (staging). # IMPORTANT: Create a service file named: __TEMPLATE_FRONTEND_STAGING_SERVICE_FILE__ -# (e.g., solo-template-frontend-staging.service) in infra/systemd/ before deploying. +# Naming convention: {appname}-{env}-{backend/frontend}.service (e.g., myapp-staging-frontend.service) # The CI/CD workflow expects this file to exist. # Copy to /etc/systemd/system/__TEMPLATE_SYSTEMD_FRONTEND_STAGING__.service on the VM. # Serves from /opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging (SvelteKit adapter-node SSR build). # Each app: /opt//{frontend,backend}/{staging,production}. +# Port: __TEMPLATE_FRONTEND_STAGING_PORT__ (dedicated port for this app/env combination) [Unit] -Description=__TEMPLATE_APP_NAME__ Frontend (staging) +Description=__TEMPLATE_APP_NAME__ Frontend (staging) - Port __TEMPLATE_FRONTEND_STAGING_PORT__ After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=/opt/__TEMPLATE_DEPLOY_APP_NAME__/frontend/staging -ExecStart=/usr/bin/node serve.mjs +ExecStart=/usr/bin/node serve.mjs --port __TEMPLATE_FRONTEND_STAGING_PORT__ Restart=on-failure RestartSec=5 EnvironmentFile=-/etc/default/__TEMPLATE_SYSTEMD_FRONTEND_STAGING__ Environment=PORT=__TEMPLATE_FRONTEND_STAGING_PORT__ +Environment=APP_ENV=staging +Environment=APP_NAME=__TEMPLATE_DEPLOY_APP_NAME__ +Environment=BACKEND_URL=http://localhost:__TEMPLATE_BACKEND_STAGING_PORT__ [Install] WantedBy=multi-user.target diff --git a/package.json b/package.json index 5126e80..85aab11 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "frontend": "pnpm --filter frontend", "backend": "pnpm --filter backend", "docs": "pnpm --filter docs", - "apply-template": "node scripts/apply-template-config.mjs" + "apply-template": "node scripts/apply-template-config.mjs", + "find-available-port": "bash scripts/find-available-port.sh" }, "devDependencies": { "yaml": "^2.8.2" diff --git a/scripts/find-available-port.sh b/scripts/find-available-port.sh new file mode 100755 index 0000000..d8105ce --- /dev/null +++ b/scripts/find-available-port.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +# +# find-available-port.sh - Find available ports for new app deployments +# +# This script scans the VM for running systemd services matching the naming +# convention {appname}-{env}-{frontend|backend}.service and determines which +# ports are in use, then suggests available ports for new deployments. +# +# Usage: ./find-available-port.sh [app-name] [environment] [component] +# app-name : Optional app name to filter (default: scan all apps) +# environment: Optional environment (staging|production, default: both) +# component : Optional component (frontend|backend, default: both) +# +# Example: +# ./find-available-port.sh # Scan all services +# ./find-available-port.sh myapp staging # Find port for myapp-staging +# ./find-available-port.sh myapp production backend # Find backend port for myapp-production +# + +set -euo pipefail + +# Default port ranges (configurable via environment variables) +FRONTEND_STAGING_PORT_START=${FRONTEND_STAGING_PORT_START:-6200} +FRONTEND_STAGING_PORT_END=${FRONTEND_STAGING_PORT_END:-6299} +FRONTEND_PRODUCTION_PORT_START=${FRONTEND_PRODUCTION_PORT_START:-6400} +FRONTEND_PRODUCTION_PORT_END=${FRONTEND_PRODUCTION_PORT_END:-6499} +BACKEND_STAGING_PORT_START=${BACKEND_STAGING_PORT_START:-6100} +BACKEND_STAGING_PORT_END=${BACKEND_STAGING_PORT_END:-6199} +BACKEND_PRODUCTION_PORT_START=${BACKEND_PRODUCTION_PORT_START:-6300} +BACKEND_PRODUCTION_PORT_END=${BACKEND_PRODUCTION_PORT_END:-6399} + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Print colored output +print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +print_success() { echo -e "${GREEN}[OK]${NC} $1"; } +print_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } +print_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Check if a port is in use +is_port_in_use() { + local port=$1 + ss -tlnp | grep -q ":${port} " 2>/dev/null || netstat -tlnp 2>/dev/null | grep -q ":${port} " +} + +# Get port from systemd service file +get_service_port() { + local service_file=$1 + local port="" + + # Try to extract PORT from Environment= lines + port=$(grep -E "^Environment=PORT=" "$service_file" 2>/dev/null | head -1 | cut -d'=' -f2) + + if [ -z "$port" ]; then + # Try to extract from ExecStart with --port flag + port=$(grep -E "^ExecStart=.*--port" "$service_file" 2>/dev/null | head -1 | grep -oE '\-\-port[[:space:]]*[0-9]+' | grep -oE '[0-9]+') + fi + + echo "$port" +} + +# Main function +main() { + local filter_app="${1:-}" + local filter_env="${2:-}" + local filter_component="${3:-}" + + print_info "Scanning for running services with naming convention: {appname}-{env}-{frontend|backend}.service" + echo "" + + # Arrays to track used ports + declare -A used_ports + declare -a running_services + + # Find all matching service files + local service_files=() + if [ -d "/etc/systemd/system" ]; then + while IFS= read -r -d '' file; do + service_files+=("$file") + done < <(find /etc/systemd/system -name "*.service" -print0 2>/dev/null) + fi + + # Also check /lib/systemd/system for installed services + if [ -d "/lib/systemd/system" ]; then + while IFS= read -r -d '' file; do + # Avoid duplicates + local basename=$(basename "$file") + local already_added=false + for sf in "${service_files[@]}"; do + if [ "$(basename "$sf")" = "$basename" ]; then + already_added=true + break + fi + done + if [ "$already_added" = false ]; then + service_files+=("$file") + fi + done < <(find /lib/systemd/system -name "*.service" -print0 2>/dev/null) + fi + + print_info "Found ${#service_files[@]} service files to analyze" + echo "" + + # Pattern: {appname}-{env}-{frontend|backend}.service + # Examples: myapp-staging-frontend.service, solo-template-production-backend.service + local pattern='^([a-zA-Z0-9_-]+)-(staging|production)-(frontend|backend)\.service$' + + echo "==========================================" + echo "Running Services (matching pattern)" + echo "==========================================" + printf "%-50s %-15s %-12s %-10s\n" "SERVICE" "STATUS" "PORT" "ENV" + echo "------------------------------------------" + + for service_file in "${service_files[@]}"; do + local filename=$(basename "$service_file") + + # Match the naming pattern + if [[ $filename =~ $pattern ]]; then + local app_name="${BASH_REMATCH[1]}" + local env="${BASH_REMATCH[2]}" + local component="${BASH_REMATCH[3]}" + + # Apply filters + if [ -n "$filter_app" ] && [ "$app_name" != "$filter_app" ]; then + continue + fi + if [ -n "$filter_env" ] && [ "$env" != "$filter_env" ]; then + continue + fi + if [ -n "$filter_component" ] && [ "$component" != "$filter_component" ]; then + continue + fi + + local service_name="${filename%.service}" + local status="inactive" + local port="N/A" + + # Check if service is active + if systemctl is-active --quiet "$service_name" 2>/dev/null; then + status="active" + elif systemctl is-enabled --quiet "$service_name" 2>/dev/null; then + status="enabled" + fi + + # Try to get port from service file + port=$(get_service_port "$service_file") + + if [ -n "$port" ]; then + used_ports["$port"]="$service_name" + + # Verify if port is actually in use + if is_port_in_use "$port"; then + print_success "Found: $service_name (Port: $port, Status: $status)" + printf "%-50s %-15s %-12s %-10s\n" "$service_name" "$status" "$port" "$env" + else + print_warning "Service $service_name configured for port $port but not listening" + printf "%-50s %-15s %-12s %-10s\n" "$service_name" "$status" "$port (not listening)" "$env" + fi + else + printf "%-50s %-15s %-12s %-10s\n" "$service_name" "$status" "N/A" "$env" + fi + + running_services+=("$service_name") + fi + done + + echo "" + echo "==========================================" + echo "Port Usage Summary" + echo "==========================================" + + if [ ${#used_ports[@]} -eq 0 ]; then + print_info "No matching services found with configured ports." + else + echo "Used ports: ${!used_ports[@]}" | tr ' ' '\n' | sort -n | tr '\n' ' ' + echo "" + echo "" + fi + + # Suggest available ports based on filters or show all ranges + echo "==========================================" + echo "Available Port Recommendations" + echo "==========================================" + + local suggest_env="${filter_env:-both}" + local suggest_component="${filter_component:-both}" + + if [ "$suggest_component" = "both" ] || [ "$suggest_component" = "frontend" ]; then + if [ "$suggest_env" = "both" ] || [ "$suggest_env" = "staging" ]; then + echo "" + echo "Frontend Staging Ports (${FRONTEND_STAGING_PORT_START}-${FRONTEND_STAGING_PORT_END}):" + local found=false + for ((port=FRONTEND_STAGING_PORT_START; port<=FRONTEND_STAGING_PORT_END; port++)); do + if [ -z "${used_ports[$port]:-}" ] && ! is_port_in_use "$port"; then + if [ "$found" = false ]; then + print_success " Available: $port" + found=true + else + echo " $port" + fi + # Show first 5 available ports + if [ "$(echo "$found" | wc -l)" -ge 5 ]; then + break + fi + fi + done + if [ "$found" = false ]; then + print_warning " No available ports in range ${FRONTEND_STAGING_PORT_START}-${FRONTEND_STAGING_PORT_END}" + fi + fi + + if [ "$suggest_env" = "both" ] || [ "$suggest_env" = "production" ]; then + echo "" + echo "Frontend Production Ports (${FRONTEND_PRODUCTION_PORT_START}-${FRONTEND_PRODUCTION_PORT_END}):" + local found=false + for ((port=FRONTEND_PRODUCTION_PORT_START; port<=FRONTEND_PRODUCTION_PORT_END; port++)); do + if [ -z "${used_ports[$port]:-}" ] && ! is_port_in_use "$port"; then + if [ "$found" = false ]; then + print_success " Available: $port" + found=true + else + echo " $port" + fi + if [ "$(echo "$found" | wc -l)" -ge 5 ]; then + break + fi + fi + done + if [ "$found" = false ]; then + print_warning " No available ports in range ${FRONTEND_PRODUCTION_PORT_START}-${FRONTEND_PRODUCTION_PORT_END}" + fi + fi + fi + + if [ "$suggest_component" = "both" ] || [ "$suggest_component" = "backend" ]; then + if [ "$suggest_env" = "both" ] || [ "$suggest_env" = "staging" ]; then + echo "" + echo "Backend Staging Ports (${BACKEND_STAGING_PORT_START}-${BACKEND_STAGING_PORT_END}):" + local found=false + for ((port=BACKEND_STAGING_PORT_START; port<=BACKEND_STAGING_PORT_END; port++)); do + if [ -z "${used_ports[$port]:-}" ] && ! is_port_in_use "$port"; then + if [ "$found" = false ]; then + print_success " Available: $port" + found=true + else + echo " $port" + fi + if [ "$(echo "$found" | wc -l)" -ge 5 ]; then + break + fi + fi + done + if [ "$found" = false ]; then + print_warning " No available ports in range ${BACKEND_STAGING_PORT_START}-${BACKEND_STAGING_PORT_END}" + fi + fi + + if [ "$suggest_env" = "both" ] || [ "$suggest_env" = "production" ]; then + echo "" + echo "Backend Production Ports (${BACKEND_PRODUCTION_PORT_START}-${BACKEND_PRODUCTION_PORT_END}):" + local found=false + for ((port=BACKEND_PRODUCTION_PORT_START; port<=BACKEND_PRODUCTION_PORT_END; port++)); do + if [ -z "${used_ports[$port]:-}" ] && ! is_port_in_use "$port"; then + if [ "$found" = false ]; then + print_success " Available: $port" + found=true + else + echo " $port" + fi + if [ "$(echo "$found" | wc -l)" -ge 5 ]; then + break + fi + fi + done + if [ "$found" = false ]; then + print_warning " No available ports in range ${BACKEND_PRODUCTION_PORT_START}-${BACKEND_PRODUCTION_PORT_END}" + fi + fi + fi + + echo "" + echo "==========================================" + echo "Usage Examples" + echo "==========================================" + echo "To deploy a new app, update template.config.yaml with:" + echo " deployment:" + echo " frontendStagingPort: " + echo " frontendProductionPort: " + echo " backendStagingPort: " + echo " backendProductionPort: " + echo "" + echo "Then run: pnpm run apply-template" + echo "" +} + +main "$@" diff --git a/template.config.example.yaml b/template.config.example.yaml index 2967806..2668390 100644 --- a/template.config.example.yaml +++ b/template.config.example.yaml @@ -39,21 +39,25 @@ deployment: # Value for /opt//... and VM_APP_NAME secret. Use a short slug (e.g. app, myapp). appName: app # Systemd unit names (used in CI/CD and example unit files). - backendStagingService: backend-staging - backendProductionService: backend-production - frontendStagingService: frontend-staging - frontendProductionService: frontend-production + # Note: Service files follow naming convention: {appName}-{env}-{frontend|backend}.service + backendStagingService: app-staging-backend + backendProductionService: app-production-backend + frontendStagingService: app-staging-frontend + frontendProductionService: app-production-frontend # Systemd service file paths (relative to infra/systemd/). # These files are copied during deploy and referenced in workflow triggers. # Defaults to {appNameSlug}-{component}-{env}.service if not specified. - frontendStagingServiceFile: solo-template-frontend-staging.service - frontendProductionServiceFile: solo-template-frontend-production.service - backendStagingServiceFile: solo-template-backend-staging.service - backendProductionServiceFile: solo-template-backend-production.service + # Recommended naming: {appName}-{env}-{component}.service + frontendStagingServiceFile: app-staging-frontend.service + frontendProductionServiceFile: app-production-frontend.service + backendStagingServiceFile: app-staging-backend.service + backendProductionServiceFile: app-production-backend.service # Frontend ports (for staging and production environments). + # Use ./scripts/find-available-port.sh to find available ports on your VM. frontendStagingPort: 6200 frontendProductionPort: 6400 # Backend API ports (for frontend to connect to). + # Use ./scripts/find-available-port.sh to find available ports on your VM. backendStagingPort: 6100 backendProductionPort: 6300 # Database names (for PostgreSQL).