diff --git a/.dockerignore b/.dockerignore index 4a9e98c..773104d 100755 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,63 @@ -node_modules -.next -.git -src -e2e -__tests__ -tools -agent-ctx -coverage -test-results -*.md -!DEPLOY.md -.env* -!.env.example -supabase/.temp +# Dependencies +node_modules/ + + +# Build output (recreated in container) +.next/ +out/ + + +# Environment files +.env +.env.* + + +# Internal directories +engineering/ +registry/ +agent-ctx/ +ferrum-platform/ +skills/ +tool-results/ + + +# Test artifacts +coverage/ +test-results/ +playwright-report/ + + +# Git +.git/ + + +# Editor +.vscode/ +.idea/ + + +# OS +.DS_Store +Thumbs.db + + +# Sandbox artifacts +screenshots/ +examples/ +mini-services/ +.zscripts/ +upload/ +download/ + + +# Misc +db/*.db +db/*.json +*.log +.budget-baseline.json +bun.lock +pnpm-lock.yaml +worklog.md +*.bak +*.zip +*.tar.gz diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bd076de..f5ae6c5 100755 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,7 +4,7 @@ on: push: branches: [main] workflow_run: - workflows: [CI] + workflows: ["CI"] types: [completed] branches: [main] @@ -14,46 +14,70 @@ concurrency: cancel-in-progress: false jobs: - deploy: - name: Deploy to Production + # ── Docker Build & Push ────────────────────────────────────── + docker: + name: Build & Push Docker Image runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 15 if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }} - environment: production + permissions: + contents: read + packages: write steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 with: - node-version: 22 - cache: npm + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Install dependencies - run: npm ci + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha + type=raw,value=latest,enable={{is_default_branch}} - - name: Build Standalone - env: - DATABASE_URL: file:/tmp/test.db - CLOUD_API_TOKEN: ${{ secrets.CLOUD_API_TOKEN }} - CLOUD_ADMIN_PASSWORD: ${{ secrets.CLOUD_ADMIN_PASSWORD }} - run: npm run build + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + target: runner - - name: Verify build artifacts - run: | - if [ ! -d ".next/standalone" ]; then - echo "FAIL: Standalone output not found" - exit 1 - fi - echo "PASS: Standalone build ready" - du -sh .next/standalone - - - name: Deploy + # ── Verify deployment ─────────────────────────────────────── + verify: + name: Verify Deploy + runs-on: ubuntu-latest + needs: [docker] + if: ${{ github.event_name == 'push' }} + steps: + - name: Deploy Summary run: | - echo "Deploy step — configure for your hosting provider" - echo "Supported targets: Vercel, Docker, AWS ECS, Railway" - echo "Standalone output is at .next/standalone/" - # Example: vercel deploy --prod - # Example: docker build -t ferrumengine . - # Example: fly deploy + echo "## Deployment Summary :shipit:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Commit** | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | \`ghcr.io/${{ github.repository }}:latest\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Author** | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Time** | $(date -u '+%Y-%m-%d %H:%M UTC') |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Pull & Run" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "docker pull ghcr.io/${{ github.repository }}:latest" >> $GITHUB_STEP_SUMMARY + echo "docker run -p 3000:3000 --env-file .env ghcr.io/${{ github.repository }}:latest" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 736b37f..6df8a71 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,7 +111,7 @@ jobs: path: ./release-artifacts/ - name: Create tarball working-directory: ./release-artifacts - run: tar -czf ../ferrumcss-${{ steps.version.outputs.VERSION }}.tar.gz .next/ public/ + run: tar -czf ../ferrumengine-${{ steps.version.outputs.VERSION }}.tar.gz .next/ public/ - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: @@ -120,5 +120,5 @@ jobs: body: ${{ steps.changelog.outputs.BODY }} draft: false prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} - files: ferrumcss-${{ steps.version.outputs.VERSION }}.tar.gz + files: ferrumengine-${{ steps.version.outputs.VERSION }}.tar.gz generate_release_notes: false diff --git a/Caddyfile b/Caddyfile index 3b84de2..6d27b54 100755 --- a/Caddyfile +++ b/Caddyfile @@ -1,15 +1,61 @@ -:8080 { - encode gzip - handle { - # Override Next.js 16 auto-CSP to allow inline RSC scripts. - # Must be inside handle block before reverse_proxy. - header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' blob:; base-uri 'self'; form-action 'self'" - - reverse_proxy 127.0.0.1:3000 { - header_up Host {host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - header_up X-Real-IP {remote_host} - } - } -} +# ═══════════════════════════════════════════════════════════════ +# FerrumEngine — Caddy Configuration (Production) +# ═══════════════════════════════════════════════════════════════ +# Usage: +# caddy run # Auto-HTTPS via Let's Encrypt +# caddy run --adapter json # JSON config +# +# Environment variables: +# FERRUM_PORT — listen port (default: 8080) +# FERRUM_HOST — upstream host (default: 127.0.0.1:3000) +# ═══════════════════════════════════════════════════════════════ + +{$FERRUM_PORT:8080} { + # ── Compression ───────────────────────────────────────── + encode gzip zstd + + # ── Override Next.js CSP for RSC inline scripts ────────── + header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' blob:; base-uri 'self'; form-action 'self'" + + # ── HSTS (Caddy auto-redirects HTTP→HTTPS, this enforces) ── + header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" + + # ── Static assets: long cache ──────────────────────────── + handle_path /_next/static/* { + header Cache-Control "public, max-age=31536000, immutable" + reverse_proxy {$FERRUM_HOST:127.0.0.1:3000} + } + + handle_path /worklets/*.js { + header Cache-Control "public, max-age=86400, stale-while-revalidate=604800" + reverse_proxy {$FERRUM_HOST:127.0.0.1:3000} + } + + # ── HTML pages: short cache for SPA rewrites ────────────── + handle_path /ferrum-effects.css { + header Cache-Control "public, max-age=86400, stale-while-revalidate=604800" + reverse_proxy {$FERRUM_HOST:127.0.0.1:3000} + } + + # ── API routes: no cache ───────────────────────────────── + handle_path /api/* { + header Cache-Control "no-store, no-cache, must-revalidate" + reverse_proxy {$FERRUM_HOST:127.0.0.1:3000} + } + + # ── Health check: no cache ──────────────────────────────── + handle_path /api/health { + header Cache-Control "no-store" + reverse_proxy {$FERRUM_HOST:127.0.0.1:3000} + } + + # ── Everything else ─────────────────────────────────────── + handle { + reverse_proxy {$FERRUM_HOST:127.0.0.1:3000} { + header_up Host {host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + header_up X-Real-IP {remote_host} + } + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fb58deb..b86690a 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,82 @@ -FROM node:18-alpine AS base +# ═══════════════════════════════════════════════════════════════ +# FerrumEngine — Multi-stage Docker Build (Standalone Output) +# ═══════════════════════════════════════════════════════════════ +# Usage: +# docker build -t ferrumengine . +# docker run -p 3000:3000 --env-file .env.production ferrumengine +# +# Production (standalone — smaller image): +# docker build --target runner -t ferrumengine:prod . +# ═══════════════════════════════════════════════════════════════ -# Install dependencies only when needed -FROM base AS deps +# ── Stage 1: Dependencies ────────────────────────────────────── +FROM node:20-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm ci --only=production +RUN npm ci -# Production image -FROM base AS runner +# ── Stage 2: Build ───────────────────────────────────────────── +FROM deps AS builder WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . -ENV NODE_ENV production +# Build args for environment variables at build time +ARG DATABASE_URL="file:/dev/null" +ARG CLOUD_API_TOKEN="" +ARG CLOUD_ADMIN_PASSWORD="" +ARG NEXT_PUBLIC_SUPABASE_URL="" +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY="" +ARG SUPABASE_SERVICE_ROLE_KEY="" +ARG TRUSTED_PROXY_IPS="" -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +ENV DATABASE_URL=${DATABASE_URL} +ENV CLOUD_API_TOKEN=${CLOUD_API_TOKEN} +ENV CLOUD_ADMIN_PASSWORD=${CLOUD_ADMIN_PASSWORD} +ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} +ENV SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} +ENV TRUSTED_PROXY_IPS=${TRUSTED_PROXY_IPS} +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 -COPY --from=deps /app/node_modules ./node_modules -COPY . . +RUN npm run build + +# ── Stage 3: Production Runner (Standalone) ──────────────────── +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy standalone output +COPY --from=builder /app/.next/standalone ./ + +# Copy static assets (public/ + .next/static/) +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/static ./.next/static + +# Copy worklet files for Paint API +COPY --from=builder /app/public/worklets ./public/worklets USER nextjs EXPOSE 3000 -ENV PORT 3000 -ENV HOSTNAME "0.0.0.0" +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:3000/api/health || exit 1 + +CMD ["node", "server.js"] -CMD ["node_modules/.bin/next", "start"] +# ── Stage 4: Development ─────────────────────────────────────── +FROM deps AS dev +WORKDIR /app +COPY . . +EXPOSE 3000 +CMD ["npm", "run", "dev"]