diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..32b367b --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,48 @@ +# Releases + +## v1.0.0 — Production Release (August 2026) + +DevForge reaches v1.0.0 with a complete AI-powered developer platform and +engineering workspace. + +### Highlights + +- **Project management** — Kanban boards, milestones, roadmaps, task + dependencies, labels and full-text search. +- **GitHub integration** — OAuth connection, repository import/sync, + pull request views, commit history and webhook-driven updates. +- **Real-time collaboration** — Socket.io-powered notifications, team + chat, typing indicators and online presence. +- **Analytics dashboard** — Velocity, health, contributor breakdowns + and repository activity, with weekly developer metrics snapshots. +- **AI repository analyzer** — Architecture, code quality, security and + documentation scores with strengths, risks and recommendations. +- **AI code review** — Severity-classified findings with a review score, + run directly on pull request diffs. +- **AI documentation generator** — Preview-and-approve README and docs + generation; approvals commit directly to GitHub. +- **AI engineering assistant** — Streamed, repo-scoped Q&A grounded in + your indexed repository chunks. +- **DevOps** — Docker Compose full-stack (postgres, api, web, ai, nginx), + Prometheus metrics, CI quality gate with security auditing. + +### Test coverage + +470 passing tests across four stacks: + +| Stack | Tests | Files | +| --- | --- | --- | +| API (Express) | 220 | 17 | +| AI service (FastAPI) | 146 | 21 | +| Web (React) | 89 | 12 | +| Database | 15 | 2 | +| **Total** | **470** | **52** | + +### Breaking changes + +None — first stable release. + +### Acknowledgements + +Built incrementally across 13 phases with a CI quality gate enforcing +lint, test, build and structure validation on every push. diff --git a/apps/ai/app/main.py b/apps/ai/app/main.py index 4a70fb6..bee993d 100644 --- a/apps/ai/app/main.py +++ b/apps/ai/app/main.py @@ -8,7 +8,7 @@ def create_app() -> FastAPI: app = FastAPI( title="DevForge AI Service", - version="0.1.0", + version="1.0.0", description="Repository ingestion, analysis and retrieval for DevForge.", ) app.include_router(health.router) diff --git a/apps/ai/pyproject.toml b/apps/ai/pyproject.toml index ad9f272..502977e 100644 --- a/apps/ai/pyproject.toml +++ b/apps/ai/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "devforge-ai" -version = "0.1.0" +version = "1.0.0" description = "DevForge AI service — ingestion, embeddings, RAG and the provider gateway" readme = "README.md" requires-python = ">=3.11" diff --git a/apps/api/package.json b/apps/api/package.json index ac6e96d..c1ea232 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@devforge/api", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "description": "DevForge API service (Express)", diff --git a/apps/api/src/app.js b/apps/api/src/app.js index 7ac9cc0..02f28d6 100644 --- a/apps/api/src/app.js +++ b/apps/api/src/app.js @@ -141,7 +141,7 @@ export function createApp({ auth = null, modules = null, realtime = null, metric app.get('/', (_req, res) => { res.json({ service: 'devforge-api', - version: '0.1.0', + version: '1.0.0', health: '/api/v1/health', }); }); diff --git a/apps/web/package.json b/apps/web/package.json index 6f726f3..4b4c70e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@devforge/web", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "description": "DevForge web application (React + Vite)", diff --git a/apps/web/src/app.jsx b/apps/web/src/app.jsx index 7e92d63..409c3c8 100644 --- a/apps/web/src/app.jsx +++ b/apps/web/src/app.jsx @@ -12,12 +12,22 @@ import { ForgotPassword } from './pages/forgot-password.jsx'; import { ResetPassword } from './pages/reset-password.jsx'; import { Projects } from './pages/projects.jsx'; import { ProjectDetail } from './pages/project-detail.jsx'; -import { TaskDetail } from './pages/task-detail.jsx'; -import { Repositories } from './pages/repositories.jsx'; -import { RepositoryDetail } from './pages/repository-detail.jsx'; -import { Chat } from './pages/chat.jsx'; -const Analytics = lazy(() => import('./pages/analytics.jsx')); +const TaskDetail = lazy(() => + import('./pages/task-detail.jsx').then((m) => ({ default: m.TaskDetail })), +); +const Repositories = lazy(() => + import('./pages/repositories.jsx').then((m) => ({ default: m.Repositories })), +); +const RepositoryDetail = lazy(() => + import('./pages/repository-detail.jsx').then((m) => ({ default: m.RepositoryDetail })), +); +const Chat = lazy(() => + import('./pages/chat.jsx').then((m) => ({ default: m.Chat })), +); +const Analytics = lazy(() => + import('./pages/analytics.jsx').then((m) => ({ default: m.Analytics })), +); function LazyFallback() { return

Loading…

; @@ -43,10 +53,10 @@ export function App() { } /> } /> } /> - } /> - } /> - } /> - } /> + }>} /> + }>} /> + }>} /> + }>} /> +
{children}
); diff --git a/apps/web/src/layouts/app-shell.jsx b/apps/web/src/layouts/app-shell.jsx index 086cd26..0077159 100644 --- a/apps/web/src/layouts/app-shell.jsx +++ b/apps/web/src/layouts/app-shell.jsx @@ -102,6 +102,12 @@ export function AppShell() { return (
+ + Skip to main content + @@ -239,6 +245,7 @@ export function AppShell() {
-
+
diff --git a/apps/web/src/test/a11y.test.jsx b/apps/web/src/test/a11y.test.jsx new file mode 100644 index 0000000..06fb427 --- /dev/null +++ b/apps/web/src/test/a11y.test.jsx @@ -0,0 +1,87 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AppShell } from '../layouts/app-shell.jsx'; + +vi.mock('../services/api.js', () => ({ + api: { + listOrganizations: vi.fn().mockResolvedValue([{ id: 'org-1', name: 'Acme Corp' }]), + listNotifications: vi.fn().mockResolvedValue({ items: [], total: 0 }), + }, +})); + +vi.mock('../services/socket.js', () => ({ + connectSocket: vi.fn(), + disconnectSocket: vi.fn(), + onRealtime: vi.fn(() => vi.fn()), +})); + +vi.mock('../stores/ui.js', () => ({ + useUiStore: vi.fn((selector) => { + const state = { theme: 'dark', toggleTheme: vi.fn() }; + return selector(state); + }), +})); + +vi.mock('../stores/auth.js', () => ({ + useAuthStore: vi.fn((selector) => { + const state = { status: 'authenticated', user: { name: 'Tester' }, logout: vi.fn() }; + return selector(state); + }), +})); + +vi.mock('../stores/workspace.js', () => ({ + useWorkspaceStore: vi.fn((selector) => { + const state = { orgId: 'org-1', selectOrg: vi.fn() }; + return selector(state); + }), +})); + +vi.mock('../stores/notifications.js', () => ({ + useNotificationsStore: Object.assign( + vi.fn(() => ({ items: [], total: 0, unread: 0, loading: false, refresh: vi.fn(), markRead: vi.fn(), markAllRead: vi.fn() })), + { getState: vi.fn(() => ({ items: [], total: 0, unread: 0 })) }, + ), +})); + +function renderShell() { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + + + , + ); +} + +describe('AppShell a11y landmarks', () => { + it('includes a skip-to-content link targeting the main element', () => { + renderShell(); + const link = screen.getByRole('link', { name: /skip to main content/i }); + expect(link).toHaveAttribute('href', '#main-content'); + }); + + it('labels the primary navigation landmark', () => { + renderShell(); + const nav = screen.getByRole('navigation', { name: /main navigation/i }); + expect(nav).toBeInTheDocument(); + }); + + it('labels the theme toggle button', () => { + renderShell(); + expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument(); + }); + + it('labels the organization selector', async () => { + renderShell(); + const select = await screen.findByRole('combobox', { name: /organization/i }); + expect(select).toBeInTheDocument(); + }); + + it('labels the notifications button', () => { + renderShell(); + expect(screen.getByRole('button', { name: /notifications/i })).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/test/app-shell.test.jsx b/apps/web/src/test/app-shell.test.jsx index 5b339b3..36e898e 100644 --- a/apps/web/src/test/app-shell.test.jsx +++ b/apps/web/src/test/app-shell.test.jsx @@ -43,11 +43,12 @@ describe('AppShell', () => { const user = userEvent.setup(); renderShell(); - const button = screen.getByRole('button', { name: 'Light' }); + const button = screen.getByRole('button', { name: /toggle theme/i }); + expect(button).toHaveTextContent('Light'); await user.click(button); expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem('devforge.theme')).toBe('light'); - expect(screen.getByRole('button', { name: 'Dark' })).toBeInTheDocument(); + expect(button).toHaveTextContent('Dark'); }); }); diff --git a/apps/web/vite.config.js b/apps/web/vite.config.js index d8007e4..ce69a73 100644 --- a/apps/web/vite.config.js +++ b/apps/web/vite.config.js @@ -8,6 +8,10 @@ export default defineConfig({ server: { port: 5173, }, + build: { + cssCodeSplit: true, + sourcemap: false, + }, test: { environment: 'jsdom', setupFiles: ['./src/test/setup.js'], diff --git a/docs/deployment/README.md b/docs/deployment/README.md new file mode 100644 index 0000000..9a5da01 --- /dev/null +++ b/docs/deployment/README.md @@ -0,0 +1,109 @@ +# Deploying DevForge + +DevForge runs as a five-service Docker Compose stack: + +| Service | Port | Description | +| --- | --- | --- | +| **nginx** | 8080 (configurable) | Reverse proxy — entry point for the browser | +| **web** | 80 (internal) | Vite-built React SPA served by nginx | +| **api** | 4000 (internal) | Express REST API + Socket.io realtime | +| **ai** | 5001 (internal) | FastAPI Python AI service | +| **postgres** | 5432 (internal) | pgvector-backed database | + +## Prerequisites + +- Docker Engine 24+ and Docker Compose v2 +- A GitHub OAuth App (for repository imports, PR webhooks and the docs/approval flow) +- An AI provider key (OpenAI or Anthropic) **or** a local Ollama instance +- A domain name with TLS termination (e.g. via a cloud load balancer in front of port 8080) + +## Quick start + +```bash +# Clone the repository +git clone https://github.com/Alaashamel/devforge.git && cd devforge + +# Copy and fill in production secrets +cp .env.example .env.production +# Edit .env.production — every secret must be set explicitly in production + +# Build and start the stack +docker compose --env-file .env.production up --build -d + +# Run database migrations +docker compose exec api node packages/database/src/migrate.js + +# (Optional) Seed demo data +docker compose exec api node packages/database/src/seed.js + +# Open the UI +open http://localhost:8080 +``` + +## Production environment + +Copy `.env.production.example` and fill in every value. The API refuses to start +in production when these secrets are left at their dev defaults: + +| Variable | Minimum | Notes | +| --- | --- | --- | +| `JWT_ACCESS_SECRET` | 32 chars | Access-token signing secret | +| `GITHUB_ENCRYPTION_KEY` | 16 chars | AES-256-GCM key for encrypted GitHub tokens | +| `AI_JOB_SECRET` | 16 chars | HMAC secret shared with the AI service | +| `API_BASE_URL` | public URL | Must not be `localhost` — used for webhooks | + +The web container is built with `VITE_API_URL=/api/v1` (relative), so it +talks to the API through the same nginx origin. No cross-origin configuration +is needed. + +## Health checks + +Every container exposes a health endpoint: + +- **api** — `GET /api/v1/health/ready` (200 = ok, 503 = degraded) +- **ai** — `GET /health` +- **nginx** — TCP port 80 (Docker default) + +Docker Compose wires these as `healthcheck` directives and gates dependent +services on `service_healthy`. + +## Monitoring + +- `GET /metrics` (api) — Prometheus text exposition format (counters, histograms, process gauges). +- Structured JSON logging via pino (`LOG_LEVEL` env var). + +Point a Prometheus instance at `http://api:4000/metrics` and scrape on a +30-second interval. + +## TLS termination + +Nginx listens on plain HTTP. In production, place a TLS-terminating reverse +proxy (cloud load balancer, Caddy, traefik, or an additional nginx with +certificates) in front of port 8080. The API sets `trust proxy 1` and reads +`X-Forwarded-Proto` for secure cookie decisions. + +## Backups + +```bash +# Dump the database +docker compose exec postgres pg_dump -U devforge devforge > backup.sql + +# Restore +cat backup.sql | docker compose exec -T postgres psql -U devforge devforge +``` + +## Scaling + +The API and AI service are stateless horizontally (sessions are JWT-based; +realtime uses Socket.io in-memory presence). For multi-instance deployments +behind a load balancer, swap in the Socket.io Redis adapter by setting +`REDIS_URL` and adding a Redis service to the compose file. + +## Troubleshooting + +| Symptom | Fix | +| --- | --- | +| API exits immediately on startup | Check that all production secrets are set in `.env.production` | +| AI service 502 from API | Verify `AI_SERVICE_URL` and `AI_JOB_SECRET` match between API and AI containers | +| Socket.io connection refused | Ensure nginx `/socket.io/` location has websocket upgrade headers | +| Database connection refused | Check `postgres` container is healthy (`docker compose ps`) | diff --git a/env.production.example b/env.production.example new file mode 100644 index 0000000..61b303b --- /dev/null +++ b/env.production.example @@ -0,0 +1,41 @@ +# ------------------------------------------------------------------ +# DevForge production environment. +# Copy to .env.production and fill in every value. The API refuses +# to start when secrets are left at their dev defaults. +# ------------------------------------------------------------------ + +# --- API ---------------------------------------------------------- +NODE_ENV=production +HOST=0.0.0.0 +PORT=4000 +LOG_LEVEL=info + +DATABASE_URL=postgres://devforge:CHANGE_ME@postgres:5432/devforge +CORS_ORIGINS=https://app.example.com + +JWT_ACCESS_SECRET= +JWT_ACCESS_TTL=15m +REFRESH_TTL_DAYS=7 +WEB_BASE_URL=https://app.example.com + +ARGON2_MEMORY_COST=65536 +ARGON2_TIME_COST=3 +ARGON2_PARALLELISM=1 + +# --- GitHub ------------------------------------------------------- +API_BASE_URL=https://api.example.com +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_OAUTH_CALLBACK_URL=https://api.example.com/api/v1/github/oauth/callback +GITHUB_ENCRYPTION_KEY= + +# --- AI service --------------------------------------------------- +AI_JOB_SECRET= +AI_JOB_TTL_SECONDS=300 +AI_ARCHIVE_TTL_SECONDS=900 +AI_PRIMARY_PROVIDER=openai +AI_OPENAI_API_KEY= +# AI_ANTHROPIC_API_KEY= + +# --- Edge proxy --------------------------------------------------- +HTTP_PORT=8080 diff --git a/package.json b/package.json index d783a03..e43ba9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devforge", - "version": "0.1.0", + "version": "1.0.0", "private": true, "description": "AI-powered developer platform and engineering workspace", "license": "MIT",