diff --git a/.github/workflows/web-bun.yml b/.github/workflows/web-bun.yml new file mode 100644 index 0000000..f0dc019 --- /dev/null +++ b/.github/workflows/web-bun.yml @@ -0,0 +1,57 @@ +# The web app (pairux.com and installer.pairux.com on dev2) builds and runs on +# Bun. pnpm stays the monorepo's package manager and Next builds on Node (see +# apps/web/Dockerfile); this checks what the image does: install with pnpm, +# build, then boot the standalone server under Bun. +name: web on Bun + +on: + push: + branches: [master] + pull_request: + paths: + - 'apps/web/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - '.github/workflows/web-bun.yml' + +permissions: + contents: read + +jobs: + build-and-boot: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + # Placeholders: the build only needs the public values to exist. + NEXT_PUBLIC_SUPABASE_URL: https://example.supabase.co + NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder + NEXT_PUBLIC_APP_URL: http://127.0.0.1:8080 + NEXT_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/checkout@v6 + # pnpm/action-setup reads pnpm@9.15.0 from packageManager. + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: '24' + cache: pnpm + # The same Bun the image pins (apps/web/Dockerfile). + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.0 + - run: pnpm install --frozen-lockfile --filter @pairux/web... + - run: pnpm --filter @pairux/shared-types build + - name: Build the web app + run: pnpm --filter @pairux/web build + - name: Standalone server answers under Bun + run: | + cd apps/web + cp -r public .next/standalone/apps/web/ + cp -r .next/static .next/standalone/apps/web/.next/ + (cd .next/standalone && PORT=8080 HOSTNAME=127.0.0.1 bun apps/web/server.js > /tmp/server.log 2>&1 &) + for i in $(seq 1 30); do + code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/health || true) + [ "$code" = 200 ] && { echo "/api/health -> $code"; exit 0; } + sleep 1 + done + cat /tmp/server.log; exit 1 diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 29b73b0..5a1b998 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,10 +1,21 @@ # =========================================== -# PairUX Web App - Railway Dockerfile +# PairUX Web App (pairux.com, installer.pairux.com on dev2) # =========================================== -# Multi-stage build for optimized production image -# Uses Node.js 24 Alpine for minimal footprint +# Runs on Bun. pnpm stays this monorepo's package manager: the desktop +# (Electron) and mobile (Expo) toolchains, the patched @expo/cli +# (pnpm.patchedDependencies) and the release scripts all depend on it, and +# none of that runs here. So Node + pnpm install dependencies and build in the +# first stages (see the build step for why Next builds on Node), and the +# standalone server runs under Bun on Alpine. +# +# Alpine 3.24 at run time, as under node:24-alpine: call analysis shells out +# to ffmpeg/ffprobe (src/lib/call-analysis/media.ts), and oven/bun:*-alpine is +# Alpine 3.22 (older ffmpeg). So the runtime is alpine:3.24 with Bun's musl +# binary copied in, the same pattern icemap.app runs in production. # =========================================== +FROM oven/bun:1.4.0-alpine AS bun + # Stage 1: Dependencies FROM node:24-alpine AS deps RUN apk add --no-cache libc6-compat @@ -55,18 +66,24 @@ ENV NODE_ENV=production # Build shared-types first (dependency of web) RUN pnpm --filter @pairux/shared-types build -# Build the web application +# Build the web application. The build stays on Node: Next 16.3's Turbopack +# runs PostCSS (Tailwind) in a separate `node` worker and hands it the parent +# process's flags, so under `bun --bun` with a real Node on PATH the worker +# dies with "node: --bun is not allowed in NODE_OPTIONS". What runs in +# production is the standalone server below, and that runs on Bun. RUN pnpm --filter @pairux/web build # Stage 3: Runner -FROM node:24-alpine AS runner +FROM alpine:3.24 AS runner WORKDIR /app -# ffmpeg/ffprobe: AI call analysis joins the uploaded call audio, measures it -# and cuts it into pieces for transcription (src/lib/call-analysis/media.ts). -RUN apk add --no-cache ffmpeg +# libstdc++/libgcc for Bun's musl binary. ffmpeg/ffprobe: AI call analysis +# joins the uploaded call audio, measures it and cuts it into pieces for +# transcription (src/lib/call-analysis/media.ts). +RUN apk add --no-cache libstdc++ libgcc ffmpeg +COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun -# Security: Run as non-root user +# Security: Run as non-root user (same uid/gid as the Node image) RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs @@ -88,9 +105,9 @@ USER nextjs # Expose port EXPOSE 8080 -# Health check +# Health check (Bun's fetch; 127.0.0.1, since Next binds IPv4 only) HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1 + CMD bun -e "fetch('http://127.0.0.1:8080/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" -# Start the application -CMD ["node", "apps/web/server.js"] +# Start the standalone server under Bun +CMD ["bun", "apps/web/server.js"] diff --git a/apps/web/package.json b/apps/web/package.json index 41be4cc..11d777d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "start": "bun --bun next start", "lint": "eslint src/", "lint:fix": "eslint src/ --fix", "typecheck": "tsc --noEmit",