diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..47d9ba5c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + pull_request: + branches: [main] + +jobs: + lint-and-typecheck: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Type check + run: npx tsc --noEmit + + build: + name: Build + runs-on: ubuntu-latest + needs: lint-and-typecheck + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + env: + VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL_PROD }} + VITE_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY_PROD }} + + test: + name: Test + runs-on: ubuntu-latest + needs: lint-and-typecheck + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test --if-present diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..ab055069 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,49 @@ +name: Build Production Image + +# Auto-build on push to main (no deploy, just build) +on: + push: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + name: Build & Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Get version + id: version + run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + build-args: | + VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL_PROD }} + VITE_SUPABASE_PUBLISHABLE_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY_PROD }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..34b95c90 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,174 @@ +name: Release + +# Manual trigger for controlled releases +on: + workflow_dispatch: + inputs: + run_migrations: + description: 'Run database migrations' + required: true + default: 'false' + type: boolean + deploy_functions: + description: 'Deploy Edge Functions' + required: true + default: 'true' + type: boolean + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # Step 1: Run all tests + test: + name: 1. Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Type check + run: npx tsc --noEmit + + - name: Build (verify) + run: npm run build + env: + VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL_PROD }} + VITE_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY_PROD }} + + - name: Run tests + run: npm run test --if-present + + # Step 2: Run migrations (if enabled, requires approval) + migrate: + name: 2. Run Migrations + runs-on: ubuntu-latest + needs: test + if: ${{ inputs.run_migrations == true }} + environment: production # Requires manual approval + steps: + - uses: actions/checkout@v4 + + - name: Setup Supabase CLI + uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Link to production project + run: supabase link --project-ref ${{ secrets.SUPABASE_PROJECT_REF_PROD }} + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + + - name: Run migrations + run: supabase db push --linked + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + + # Step 3: Deploy Edge Functions (if enabled) + deploy-functions: + name: 3. Deploy Edge Functions + runs-on: ubuntu-latest + needs: [test, migrate] + if: | + always() && + needs.test.result == 'success' && + (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && + inputs.deploy_functions == true + steps: + - uses: actions/checkout@v4 + + - name: Setup Supabase CLI + uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Deploy Edge Functions + run: supabase functions deploy --project-ref ${{ secrets.SUPABASE_PROJECT_REF_PROD }} + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + + # Step 4: Build and push Docker image + build-and-push: + name: 4. Build & Push Docker Image + runs-on: ubuntu-latest + needs: [test, migrate, deploy-functions] + if: | + always() && + needs.test.result == 'success' && + (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && + (needs.deploy-functions.result == 'success' || needs.deploy-functions.result == 'skipped') + permissions: + contents: read + packages: write + outputs: + version: ${{ steps.version.outputs.VERSION }} + steps: + - uses: actions/checkout@v4 + + - name: Get version + id: version + run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + build-args: | + VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL_PROD }} + VITE_SUPABASE_PUBLISHABLE_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY_PROD }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Step 5: Create GitHub release + create-release: + name: 5. Create Release + runs-on: ubuntu-latest + needs: build-and-push + if: needs.build-and-push.result == 'success' + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.build-and-push.outputs.version }} + generate_release_notes: true + body: | + ## Release v${{ needs.build-and-push.outputs.version }} + + **What was deployed:** + - Migrations: ${{ inputs.run_migrations }} + - Edge Functions: ${{ inputs.deploy_functions }} + + **Docker Image:** + ``` + docker pull ghcr.io/${{ github.repository }}:${{ needs.build-and-push.outputs.version }} + ``` diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..fc14f1e9 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,12 @@ +# Replace app.eryxon-flow.com with your domain +app.eryxon-flow.com { + reverse_proxy app:80 + + # Security headers (Caddy adds many by default) + header { + X-Frame-Options "DENY" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Referrer-Policy "strict-origin-when-cross-origin" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f4a5e9a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build arguments for Supabase config (passed at build time) +ARG VITE_SUPABASE_URL +ARG VITE_SUPABASE_PUBLISHABLE_KEY + +# Set as environment variables for Vite build +ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL +ENV VITE_SUPABASE_PUBLISHABLE_KEY=$VITE_SUPABASE_PUBLISHABLE_KEY + +# Build the app +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built assets from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 0580e9e9..13dda229 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Comprehensive documentation is available in the [`/docs`](./docs) folder: - **[API_DOCUMENTATION.md](docs/API_DOCUMENTATION.md)** - REST API reference - **[DESIGN_SYSTEM.md](docs/DESIGN_SYSTEM.md)** - Design tokens and styling - **[EDGE_FUNCTIONS_SETUP.md](docs/EDGE_FUNCTIONS_SETUP.md)** - Edge Functions guide +- **[CICD_DEPLOYMENT_PLAN.md](docs/CICD_DEPLOYMENT_PLAN.md)** - CI/CD pipeline and Docker deployment - **[CLAUDE.md](CLAUDE.md)** - AI assistant guide for contributors Additional documentation: @@ -117,11 +118,26 @@ Additional documentation: ## 📦 Deployment -Deployed via [Lovable Platform](https://lovable.dev/projects/aaa3208a-70fb-4eb6-a5eb-5823f025e734) +| Environment | Platform | Details | +|-------------|----------|---------| +| **Development** | [Lovable](https://lovable.dev/projects/aaa3208a-70fb-4eb6-a5eb-5823f025e734) | Auto-syncs with GitHub | +| **Production** | Docker on Hetzner | EU-hosted, fixed costs | +| **Local/On-Premise** | Docker | Same image, custom Supabase | -To deploy updates: **Share → Publish** +### CI/CD Pipeline -For custom domains, see [Lovable docs](https://docs.lovable.dev/features/custom-domain) +- **PRs**: Automated lint, type-check, build, test +- **Releases**: Manual workflow with migrations and Edge Functions deployment +- **Docker Images**: Published to GitHub Container Registry (GHCR) + +See **[docs/CICD_DEPLOYMENT_PLAN.md](docs/CICD_DEPLOYMENT_PLAN.md)** for full setup instructions. + +### Quick Docker Run + +```bash +docker pull ghcr.io/sheetmetalconnect/eryxon-flow:latest +docker run -p 8080:80 ghcr.io/sheetmetalconnect/eryxon-flow:latest +``` ## 📄 License @@ -133,6 +149,6 @@ This software is for internal use only and may not be distributed, copied, or mo --- -**Built with** React 18 + TypeScript + Supabase -**Status**: Production +**Built with** React 18 + TypeScript + Supabase +**Status**: Production **Version**: 1.2 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..25bdc140 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,33 @@ +# Eryxon Flow - Production with HTTPS +# Use this for production with automatic SSL via Caddy + +services: + app: + image: ghcr.io/sheetmetalconnect/eryxon-flow:latest + container_name: eryxon-flow + restart: unless-stopped + expose: + - "80" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + + caddy: + image: caddy:alpine + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - app + +volumes: + caddy_data: + caddy_config: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..1a15c421 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +# Eryxon Flow - Production Deployment +# Deploy on Hetzner with Docker + +services: + eryxon-flow: + image: ghcr.io/sheetmetalconnect/eryxon-flow:latest + container_name: eryxon-flow + restart: unless-stopped + ports: + - "80:80" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # Optional: Caddy reverse proxy for HTTPS (recommended) + # Uncomment below to enable automatic SSL with Let's Encrypt + # + # caddy: + # image: caddy:alpine + # container_name: caddy + # restart: unless-stopped + # ports: + # - "80:80" + # - "443:443" + # volumes: + # - ./Caddyfile:/etc/caddy/Caddyfile + # - caddy_data:/data + # - caddy_config:/config + # depends_on: + # - eryxon-flow + +# volumes: +# caddy_data: +# caddy_config: diff --git a/docs/CICD_DEPLOYMENT_PLAN.md b/docs/CICD_DEPLOYMENT_PLAN.md new file mode 100644 index 00000000..1b02efdc --- /dev/null +++ b/docs/CICD_DEPLOYMENT_PLAN.md @@ -0,0 +1,236 @@ +# CI/CD Deployment Plan - Eryxon Flow + +## Architecture + +``` +Feature Branches → main branch + ↓ ↓ + CI Tests Build Docker Image + (push to GHCR) + + ┌─────────────────────────────────────────────┐ + │ Manual Release Workflow │ + └─────────────────────────────────────────────┘ + ↓ + 1. Run Tests + ↓ + 2. Run Migrations (optional) + ↓ + 3. Deploy Edge Functions + ↓ + 4. Build & Push Docker Image + ↓ + 5. Create GitHub Release +``` + +## Environments + +| Environment | Frontend | Database | Purpose | +|-------------|----------|----------|---------| +| **DEV** | Lovable | Current Supabase | Development & testing | +| **PROD** | Docker (Hetzner) | New Supabase (EU) | Production | +| **Local** | Docker | DEV or PROD Supabase | Local testing | +| **On-Premise** | Docker | Customer Supabase | Customer deployments | + +## Workflows + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `ci.yml` | PRs | Lint, type-check, build, test | +| `deploy-prod.yml` | Push to `main` | Auto-build Docker image to GHCR | +| `release.yml` | **Manual** | Full controlled release with migrations | + +--- + +## Release Process + +### Quick Release (no migrations) +1. Go to **Actions → Release → Run workflow** +2. Set `run_migrations: false` +3. Set `deploy_functions: true` +4. Click **Run workflow** + +### Full Release (with migrations) +1. Go to **Actions → Release → Run workflow** +2. Set `run_migrations: true` +3. Set `deploy_functions: true` +4. Click **Run workflow** +5. **Approve** the migration step (GitHub Environment protection) + +--- + +## GitHub Secrets + +| Secret | Value | +|--------|-------| +| `VITE_SUPABASE_URL_PROD` | Supabase URL (EU region) | +| `VITE_SUPABASE_ANON_KEY_PROD` | Supabase anon key | +| `SUPABASE_PROJECT_REF_PROD` | Supabase project ref | +| `SUPABASE_ACCESS_TOKEN` | Supabase CLI token | + +--- + +## Setup Steps + +### 1. Create Supabase Production Project (EU) +1. Go to [supabase.com](https://supabase.com) +2. Create new project → Select **EU region** (Frankfurt) +3. Note: project URL, anon key, project ref + +### 2. Configure GitHub +1. Add secrets: Settings → Secrets → Actions +2. Create Environment "production" with required reviewers + +### 3. Initial Production Migration +```bash +supabase link --project-ref YOUR_PROD_PROJECT_REF +supabase db push +``` + +--- + +## Docker Image + +### Pull from GHCR +```bash +# Latest +docker pull ghcr.io/sheetmetalconnect/eryxon-flow:latest + +# Specific version +docker pull ghcr.io/sheetmetalconnect/eryxon-flow:1.0.0 +``` + +### Run Locally (for testing) +```bash +docker run -p 8080:80 ghcr.io/sheetmetalconnect/eryxon-flow:latest +# Open http://localhost:8080 +``` + +### Build Locally (custom Supabase) +```bash +docker build -t eryxon-flow \ + --build-arg VITE_SUPABASE_URL=https://your-project.supabase.co \ + --build-arg VITE_SUPABASE_PUBLISHABLE_KEY=your-anon-key . + +docker run -p 8080:80 eryxon-flow +``` + +--- + +## Hetzner Production Deployment + +### 1. Create Server +1. [Hetzner Cloud Console](https://console.hetzner.cloud) +2. Create server: Ubuntu 24.04, CX22 (~€4/mo), EU region +3. Add SSH key + +### 2. Server Setup +```bash +ssh root@YOUR_SERVER_IP + +# Install Docker +curl -fsSL https://get.docker.com | sh + +# Create app directory +mkdir -p /opt/eryxon-flow +cd /opt/eryxon-flow + +# Login to GHCR +docker login ghcr.io -u YOUR_GITHUB_USERNAME + +# Create docker-compose.yml +cat > docker-compose.yml << 'EOF' +services: + app: + image: ghcr.io/sheetmetalconnect/eryxon-flow:latest + container_name: eryxon-flow + restart: unless-stopped + expose: + - "80" + + caddy: + image: caddy:alpine + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + +volumes: + caddy_data: +EOF + +# Create Caddyfile +cat > Caddyfile << 'EOF' +app.yourdomain.com { + reverse_proxy app:80 +} +EOF + +# Start +docker compose up -d +``` + +### 3. DNS +Point `app.yourdomain.com` → Server IP. Caddy handles SSL automatically. + +### 4. Update Production +```bash +cd /opt/eryxon-flow +docker compose pull +docker compose up -d --remove-orphans +``` + +--- + +## Customer On-Premise Deployment + +Same Docker image, customer provides their own Supabase: + +```bash +# Build with customer's Supabase +docker build -t eryxon-flow-customer \ + --build-arg VITE_SUPABASE_URL=https://customer-project.supabase.co \ + --build-arg VITE_SUPABASE_PUBLISHABLE_KEY=customer-anon-key . + +# Or use docker-compose with env vars +cat > .env << EOF +SUPABASE_URL=https://customer-project.supabase.co +SUPABASE_ANON_KEY=customer-anon-key +EOF +``` + +--- + +## File Structure + +``` +.github/workflows/ + ci.yml # PR checks + deploy-prod.yml # Main → Docker image (auto) + release.yml # Manual release with migrations +Dockerfile # Multi-stage build +nginx.conf # SPA routing +docker-compose.yml # Simple deployment +docker-compose.prod.yml # With Caddy SSL +Caddyfile # Caddy config +``` + +--- + +## Developer Workflow + +```bash +# Development (use Lovable) +# Push to GitHub → Lovable auto-syncs + +# Ready for production release +# Actions → Release → Run workflow +# Docker image built and pushed to GHCR + +# Deploy to Hetzner +ssh root@server "cd /opt/eryxon-flow && docker compose pull && docker compose up -d" +``` diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..35ceaebf --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml; + + # Security headers + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache static assets + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "OK"; + add_header Content-Type text/plain; + } +}