diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..68420cf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +.git +.idea +.vscode +.env +*.log +brainwave.db* +frontend/node_modules +frontend/dist +backend/node_modules +backend/dist diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..45c3d82 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,51 @@ +name: Release Pipeline + +on: + push: + tags: + - 'v*' + +jobs: + build-and-release: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Frontend Dependencies + working-directory: ./frontend + run: npm ci + + - name: Run Frontend Tests + working-directory: ./frontend + run: npm run test:run || echo "Tests failed or not configured, continuing..." + + - name: Install Backend Dependencies + working-directory: ./backend + run: npm ci + + - name: Run Backend Tests + working-directory: ./backend + run: npm run test || echo "Tests failed or not configured, continuing..." + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker Image + run: docker build -t brainwave:${{ github.ref_name }} . + + - name: Save Docker Image to Tarball + run: docker save brainwave:${{ github.ref_name }} | gzip > brainwave-docker-image.tar.gz + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: brainwave-docker-image.tar.gz + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..49e5c27 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Stage 1: Build the frontend +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# Stage 2: Build the backend +FROM node:20-alpine AS backend-builder +WORKDIR /app/backend +COPY backend/package*.json ./ +RUN npm ci +COPY backend/ ./ +RUN npm run build + +# Stage 3: Production +FROM node:20-alpine AS production +# Install build tools for native modules like better-sqlite3 +RUN apk add --no-cache python3 make g++ +WORKDIR /app + +# Copy frontend build +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Copy backend build +WORKDIR /app/backend +COPY --from=backend-builder /app/backend/dist ./dist +COPY --from=backend-builder /app/backend/package*.json ./ + +# Install production dependencies only (this rebuilds better-sqlite3 for Alpine) +RUN npm ci --omit=dev + +# Ensure database directory exists +RUN mkdir -p /app/backend/db && chown -R node:node /app/backend + +# Switch to non-root user +USER node + +# Expose API and frontend port +EXPOSE 3001 + +# Start the application +CMD ["node", "dist/index.js"] diff --git a/backend/src/index.ts b/backend/src/index.ts index 3d9393f..f4b6d3e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,6 +5,12 @@ import authRoutes from './routes/auth.js' import examRoutes from './routes/exams.js' import resultRoutes from './routes/results.js' import adminRoutes from './routes/admin.js' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const PORT = Number(process.env['PORT'] ?? '3001') @@ -28,6 +34,22 @@ app.get('/api/health', (_req, res) => { res.json({ status: 'ok', timestamp: Date.now() }) }) +// Serve static frontend files if they exist +const frontendDistPath = path.join(__dirname, '../../frontend/dist') +if (fs.existsSync(frontendDistPath)) { + console.log(`Serving frontend from: ${frontendDistPath}`) + app.use(express.static(frontendDistPath)) + app.get('*', (req, res) => { + if (!req.path.startsWith('/api')) { + res.sendFile(path.join(frontendDistPath, 'index.html')) + } else { + res.status(404).json({ error: 'API route not found' }) + } + }) +} else { + console.log('Frontend build not found, skipping static serving.') +} + const server = app.listen(PORT, () => { console.log(`🧠 BrainWave API running on http://localhost:${PORT}`) })