diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..828d1ab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + backend: + name: Backend + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + env: + NODE_ENV: test + JWT_SECRET: ci-test-secret + DB_PROVIDER: memory + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: backend/package-lock.json + - run: npm ci + - run: npm run typecheck + - run: npm test + - run: npm run build + + web: + name: Web + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + - run: npm run typecheck + - run: npm test + - run: npm run build + + mobile: + name: Mobile Typecheck + runs-on: ubuntu-latest + defaults: + run: + working-directory: mobile + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: mobile/package-lock.json + - run: npm ci + - run: npm run typecheck diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7e54258 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +# ── Stage 1: Install ────────────────────────────────── +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --production=false + +# ── Stage 2: Build ──────────────────────────────────── +FROM node:20-alpine AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +# ── Stage 3: Runtime ────────────────────────────────── +FROM node:20-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=build /app/dist ./dist +COPY --from=deps /app/node_modules ./node_modules +COPY package.json ./ + +EXPOSE 4016 + +CMD ["node", "dist/server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3a0ebb6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: "3.9" + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "4016:4016" + environment: + - NODE_ENV=production + - PORT=4016 + - HOST=0.0.0.0 + - JWT_SECRET=${JWT_SECRET:-dev-secret-change-me} + - COSMOS_ENDPOINT=${COSMOS_ENDPOINT:-} + - COSMOS_KEY=${COSMOS_KEY:-} + - COSMOS_DATABASE=${COSMOS_DATABASE:-bytelyst} + - DB_PROVIDER=${DB_PROVIDER:-memory} + - CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:3000} + - PLATFORM_SERVICE_URL=${PLATFORM_SERVICE_URL:-http://localhost:4003} + - EXTRACTION_SERVICE_URL=${EXTRACTION_SERVICE_URL:-http://localhost:4005} + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:4016/health"] + interval: 30s + timeout: 5s + retries: 3 + + web: + build: + context: ./web + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_BACKEND_URL=http://backend:4016 + - NEXT_PUBLIC_PLATFORM_URL=${PLATFORM_SERVICE_URL:-http://localhost:4003} + depends_on: + backend: + condition: service_healthy + restart: unless-stopped diff --git a/scripts/docker-prep.sh b/scripts/docker-prep.sh new file mode 100755 index 0000000..6d1e766 --- /dev/null +++ b/scripts/docker-prep.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +# docker-prep.sh — Pack @bytelyst/* packages into tarballs for Docker builds +# Usage: +# ./scripts/docker-prep.sh # pack tarballs + rewrite package.json +# ./scripts/docker-prep.sh --restore # undo changes + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +COMMON_PLAT="${ROOT_DIR}/../learning_ai_common_plat" +TARBALL_DIR="${ROOT_DIR}/.tarballs" + +if [[ "${1:-}" == "--restore" ]]; then + echo "Restoring package.json files..." + for pkg_json in backend/package.json web/package.json; do + backup="${ROOT_DIR}/${pkg_json}.bak" + if [[ -f "$backup" ]]; then + mv "$backup" "${ROOT_DIR}/${pkg_json}" + echo " Restored ${pkg_json}" + fi + done + rm -rf "$TARBALL_DIR" + echo "Done." + exit 0 +fi + +if [[ ! -d "$COMMON_PLAT/packages" ]]; then + echo "ERROR: Cannot find $COMMON_PLAT/packages — run from the correct directory" + exit 1 +fi + +echo "Building @bytelyst/* packages..." +(cd "$COMMON_PLAT" && pnpm build) + +echo "Packing tarballs..." +rm -rf "$TARBALL_DIR" +mkdir -p "$TARBALL_DIR" + +for pkg_dir in "$COMMON_PLAT"/packages/*/; do + pkg_name=$(basename "$pkg_dir") + if [[ -f "$pkg_dir/package.json" ]]; then + (cd "$pkg_dir" && npm pack --pack-destination "$TARBALL_DIR" 2>/dev/null) || true + fi +done + +echo "Tarballs created:" +ls -la "$TARBALL_DIR"/*.tgz 2>/dev/null || echo " (none)" + +echo "Rewriting package.json file: refs..." +for pkg_json in backend/package.json web/package.json; do + full_path="${ROOT_DIR}/${pkg_json}" + if [[ ! -f "$full_path" ]]; then + continue + fi + + cp "$full_path" "${full_path}.bak" + + # Replace file:../../learning_ai_common_plat/packages/* refs with tarball paths + node -e " + const fs = require('fs'); + const path = require('path'); + const pkg = JSON.parse(fs.readFileSync('$full_path', 'utf8')); + const tarballDir = '$TARBALL_DIR'; + const tarballs = fs.readdirSync(tarballDir).filter(f => f.endsWith('.tgz')); + + for (const depType of ['dependencies', 'devDependencies']) { + if (!pkg[depType]) continue; + for (const [name, version] of Object.entries(pkg[depType])) { + if (typeof version === 'string' && version.startsWith('file:')) { + const shortName = name.replace('@bytelyst/', 'bytelyst-'); + const tarball = tarballs.find(t => t.startsWith(shortName)); + if (tarball) { + pkg[depType][name] = 'file:' + path.join(tarballDir, tarball); + } + } + } + } + + fs.writeFileSync('$full_path', JSON.stringify(pkg, null, 2) + '\n'); + " + echo " Rewrote ${pkg_json}" +done + +echo "Done. Run 'docker build' now. Use --restore to undo." diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..5a1fb46 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,30 @@ +# ── Stage 1: Install ────────────────────────────────── +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +# ── Stage 2: Build ──────────────────────────────────── +FROM node:20-alpine AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Dummy env vars for Next.js build-time page data collection +ENV NEXT_PUBLIC_BACKEND_URL=http://localhost:4016 +ENV NEXT_PUBLIC_PLATFORM_URL=http://localhost:4003 + +RUN npm run build + +# ── Stage 3: Runtime ────────────────────────────────── +FROM node:20-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static +COPY --from=build /app/public ./public + +EXPOSE 3000 + +CMD ["node", "server.js"]