diff --git a/docs/plans/2026-02-20-devops-features.md b/docs/plans/2026-02-20-devops-features.md new file mode 100644 index 0000000..36ee362 --- /dev/null +++ b/docs/plans/2026-02-20-devops-features.md @@ -0,0 +1,948 @@ +# DevOps Features Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement Swagger docs, Knex migrations, quality gates, and secret scanning. + +**Architecture:** Swagger UI served at /api/docs with JSDoc-annotated routes. Knex.js handles database migrations with auto-migration on startup. Quality gates integrated into CI workflow. Gitleaks pre-commit hook for secret scanning. + +**Tech Stack:** swagger-jsdoc, swagger-ui-express, knex, vitest coverage, bundlesize, gitleaks + +--- + +## Task 1: Install Backend Dependencies + +**Files:** +- Modify: `backend/package.json` + +**Step 1: Install Knex for migrations** + +```bash +cd backend && npm install knex swagger-jsdoc swagger-ui-express +``` + +**Step 2: Verify installation** + +Run: `cd backend && npm list knex swagger-jsdoc swagger-ui-express --depth=0` +Expected: All packages listed with versions + +**Step 3: Commit** + +```bash +git add backend/package.json backend/package-lock.json +git commit -m "build(deps): add knex, swagger-jsdoc, swagger-ui-express" +``` + +--- + +## Task 2: Install Frontend Quality Gate Dependencies + +**Files:** +- Modify: `package.json` + +**Step 1: Install coverage and bundlesize packages** + +```bash +npm install --save-dev @vitest/coverage-v8 bundlesize +``` + +**Step 2: Verify installation** + +Run: `npm list @vitest/coverage-v8 bundlesize --depth=0` +Expected: All packages listed with versions + +**Step 3: Commit** + +```bash +git add package.json package-lock.json +git commit -m "build(deps): add coverage and bundlesize for quality gates" +``` + +--- + +## Task 3: Create Knex Configuration + +**Files:** +- Create: `backend/knexfile.js` + +**Step 1: Create knexfile.js** + +Create `backend/knexfile.js`: + +```javascript +export default { + client: 'better-sqlite3', + connection: { + filename: process.env.DB_PATH || './data/cv.db' + }, + migrations: { + directory: './migrations', + tableName: 'knex_migrations' + }, + seeds: { + directory: './seeds' + }, + useNullAsDefault: true +}; +``` + +**Step 2: Verify syntax** + +Run: `cd backend && node -e "import('./knexfile.js').then(m => console.log(m.default))"` +Expected: Knex config object printed + +**Step 3: Commit** + +```bash +git add backend/knexfile.js +git commit -m "feat(db): add knex configuration" +``` + +--- + +## Task 4: Create Initial Migration + +**Files:** +- Create: `backend/migrations/20260220000001_initial_schema.js` + +**Step 1: Create migrations directory** + +```bash +mkdir -p backend/migrations +``` + +**Step 2: Create initial migration** + +Create `backend/migrations/20260220000001_initial_schema.js`: + +```javascript +export async function up(knex) { + await knex.schema.createTable('cv_data', (table) => { + table.integer('id').primary(); + table.text('data').notNullable(); + table.datetime('updated_at').defaultTo(knex.fn.now()); + }); +} + +export async function down(knex) { + await knex.schema.dropTableIfExists('cv_data'); +} +``` + +**Step 3: Commit** + +```bash +git add backend/migrations/ +git commit -m "feat(db): add initial schema migration" +``` + +--- + +## Task 5: Create Seed File + +**Files:** +- Create: `backend/seeds/initial_cv_data.js` + +**Step 1: Create seeds directory** + +```bash +mkdir -p backend/seeds +``` + +**Step 2: Create seed file** + +Create `backend/seeds/initial_cv_data.js`: + +```javascript +export async function seed(knex) { + const existing = await knex('cv_data').where({ id: 1 }).first(); + + if (!existing) { + await knex('cv_data').insert({ + id: 1, + data: JSON.stringify({ + personal: { + name: "Tuan-Dat Tran", + title: "Junior DevOps Engineer", + intro: "Passionierter DevOps Engineer mit Fokus auf Cloud-Infrastruktur, Container-Orchestrierung und automatisierte Deployment-Pipelines.", + email: "tuan-dat.tran@example.com", + github: "https://github.com/tuan-dat-tran", + linkedin: "https://linkedin.com/in/tuan-dat-tran", + location: "Deutschland" + }, + experience: [], + skills: {}, + education: [], + projects: [] + }) + }); + } +} +``` + +**Step 3: Commit** + +```bash +git add backend/seeds/ +git commit -m "feat(db): add initial cv data seed" +``` + +--- + +## Task 6: Update Database Init to Use Knex + +**Files:** +- Modify: `backend/db/init.js` + +**Step 1: Replace init.js with Knex-based version** + +Replace `backend/db/init.js` with: + +```javascript +import knex from 'knex'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import config from '../knexfile.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +let db = null; + +export async function getDB() { + if (!db) { + db = knex(config); + await db.migrate.latest(); + } + return db; +} + +export async function initDB() { + const db = await getDB(); + + const existing = await db('cv_data').where({ id: 1 }).first(); + if (!existing) { + await db('cv_data').insert({ + id: 1, + data: JSON.stringify({ + personal: { + name: "Tuan-Dat Tran", + title: "Junior DevOps Engineer", + intro: "Passionierter DevOps Engineer mit Fokus auf Cloud-Infrastruktur.", + email: "tuan-dat.tran@example.com", + github: "https://github.com/tuan-dat-tran", + linkedin: "https://linkedin.com/in/tuan-dat-tran", + location: "Deutschland" + }, + experience: [], + skills: {}, + education: [], + projects: [] + }) + }); + console.log('Initialized database with default CV data'); + } + + console.log(`Database initialized`); +} + +export async function closeDB() { + if (db) { + await db.destroy(); + db = null; + } +} +``` + +**Step 2: Verify syntax** + +Run: `cd backend && node -c db/init.js` +Expected: No syntax errors + +**Step 3: Commit** + +```bash +git add backend/db/init.js +git commit -m "refactor(db): use knex for database operations" +``` + +--- + +## Task 7: Update CV Routes for Knex + +**Files:** +- Modify: `backend/routes/cv.js` + +**Step 1: Update routes to use Knex** + +Replace `backend/routes/cv.js` with: + +```javascript +import { Router } from 'express'; +import { getDB } from '../db/init.js'; +import { authMiddleware } from '../middleware/auth.js'; + +const router = Router(); + +/** + * @openapi + * /cv: + * get: + * summary: Get CV data + * tags: [CV] + * responses: + * 200: + * description: CV data + * content: + * application/json: + * schema: + * type: object + * 404: + * description: CV data not found + */ +router.get('/', async (req, res) => { + try { + const db = await getDB(); + const row = await db('cv_data').where({ id: 1 }).first(); + + if (!row) { + return res.status(404).json({ error: 'CV data not found' }); + } + + res.json(JSON.parse(row.data)); + } catch (error) { + console.error('Error fetching CV:', error); + res.status(500).json({ error: 'Failed to fetch CV data' }); + } +}); + +/** + * @openapi + * /cv: + * put: + * summary: Update CV data + * tags: [CV] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: CV data updated + * 401: + * description: Unauthorized + */ +router.put('/', authMiddleware, async (req, res) => { + try { + const data = req.body; + + if (!data || typeof data !== 'object') { + return res.status(400).json({ error: 'Invalid CV data' }); + } + + if (!data.personal?.name) { + return res.status(400).json({ error: 'personal.name is required' }); + } + + const db = await getDB(); + await db('cv_data').where({ id: 1 }).update({ + data: JSON.stringify(data), + updated_at: db.fn.now() + }); + + res.json({ success: true, message: 'CV data updated' }); + } catch (error) { + console.error('Error updating CV:', error); + res.status(500).json({ error: 'Failed to update CV data' }); + } +}); + +/** + * @openapi + * /cv/export: + * get: + * summary: Export CV data as JSON file + * tags: [CV] + * responses: + * 200: + * description: CV JSON file + * content: + * application/json: + * schema: + * type: object + */ +router.get('/export', async (req, res) => { + try { + const db = await getDB(); + const row = await db('cv_data').where({ id: 1 }).first(); + + if (!row) { + return res.status(404).json({ error: 'CV data not found' }); + } + + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Disposition', 'attachment; filename="cv.json"'); + res.send(row.data); + } catch (error) { + console.error('Error exporting CV:', error); + res.status(500).json({ error: 'Failed to export CV data' }); + } +}); + +/** + * @openapi + * /cv/import: + * post: + * summary: Import CV data from JSON + * tags: [CV] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: CV data imported + * 401: + * description: Unauthorized + */ +router.post('/import', authMiddleware, async (req, res) => { + try { + const data = req.body; + + if (!data || typeof data !== 'object') { + return res.status(400).json({ error: 'Invalid CV data' }); + } + + if (!data.personal?.name) { + return res.status(400).json({ error: 'personal.name is required' }); + } + + const db = await getDB(); + await db('cv_data').where({ id: 1 }).update({ + data: JSON.stringify(data), + updated_at: db.fn.now() + }); + + res.json({ success: true, message: 'CV data imported' }); + } catch (error) { + console.error('Error importing CV:', error); + res.status(500).json({ error: 'Failed to import CV data' }); + } +}); + +export default router; +``` + +**Step 2: Commit** + +```bash +git add backend/routes/cv.js +git commit -m "refactor(api): update cv routes for knex with openapi docs" +``` + +--- + +## Task 8: Create Swagger Docs Route + +**Files:** +- Create: `backend/routes/docs.js` + +**Step 1: Create docs route** + +Create `backend/routes/docs.js`: + +```javascript +import { Router } from 'express'; +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; + +const router = Router(); + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'CV API', + version: '1.0.0', + description: 'API for CV/Resume management', + }, + servers: [ + { url: '/api', description: 'API server' } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + }, + }, + }, + }, + apis: ['./routes/*.js'], +}; + +const specs = swaggerJsdoc(options); + +router.use('/docs', swaggerUi.serve, swaggerUi.setup(specs, { + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'CV API Documentation' +})); + +router.get('/docs.json', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(specs); +}); + +export default router; +``` + +**Step 2: Commit** + +```bash +git add backend/routes/docs.js +git commit -m "feat(api): add swagger docs route" +``` + +--- + +## Task 9: Add OpenAPI Docs to Auth Routes + +**Files:** +- Modify: `backend/routes/auth.js` + +**Step 1: Add JSDoc comments to auth routes** + +Read `backend/routes/auth.js` and add OpenAPI JSDoc comments to each endpoint: + +- Add `@openapi` comments for `/auth/config`, `/auth/login` +- Document request bodies and responses + +**Step 2: Commit** + +```bash +git add backend/routes/auth.js +git commit -m "docs(api): add openapi docs to auth routes" +``` + +--- + +## Task 10: Register Swagger Route in Server + +**Files:** +- Modify: `backend/server.js` + +**Step 1: Import and use docs route** + +Add to `backend/server.js`: + +```javascript +import docsRoutes from './routes/docs.js'; + +// After other route imports +app.use('/api', docsRoutes); +``` + +**Step 2: Commit** + +```bash +git add backend/server.js +git commit -m "feat(api): register swagger docs route" +``` + +--- + +## Task 11: Configure Vitest Coverage + +**Files:** +- Modify: `vitest.config.js` + +**Step 1: Add coverage configuration** + +Update `vitest.config.js` to include coverage settings: + +```javascript +coverage: { + provider: 'v8', + reporter: ['text', 'lcov', 'html'], + exclude: [ + 'node_modules/', + 'tests/', + '**/*.test.js', + '**/*.config.js' + ], + lines: 80, + functions: 80, + branches: 80, + statements: 80 +} +``` + +**Step 2: Add coverage script to package.json** + +Add script: `"test:coverage": "vitest run --coverage"` + +**Step 3: Commit** + +```bash +git add vitest.config.js package.json +git commit -m "feat(ci): add coverage configuration" +``` + +--- + +## Task 12: Configure Bundle Size Checking + +**Files:** +- Modify: `package.json` + +**Step 1: Add bundlesize configuration** + +Add to `package.json`: + +```json +"bundlesize": [ + { + "path": "./dist/assets/*.js", + "maxSize": "500kb" + }, + { + "path": "./dist/assets/*.css", + "maxSize": "100kb" + } +] +``` + +**Step 2: Add bundle check script** + +Add script: `"bundle:check": "npm run build && bundlesize"` + +**Step 3: Commit** + +```bash +git add package.json +git commit -m "feat(ci): add bundlesize configuration" +``` + +--- + +## Task 13: Create Lighthouse CI Configuration + +**Files:** +- Create: `.lighthouserc.json` + +**Step 1: Create lighthouse config** + +Create `.lighthouserc.json`: + +```json +{ + "ci": { + "collect": { + "url": ["http://localhost:4173/cv/"], + "numberOfRuns": 3 + }, + "assert": { + "assertions": { + "categories:performance": ["error", { "minScore": 0.8 }], + "categories:accessibility": ["warn", { "minScore": 0.9 }], + "categories:best-practices": ["warn", { "minScore": 0.9 }] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} +``` + +**Step 2: Commit** + +```bash +git add .lighthouserc.json +git commit -m "feat(ci): add lighthouse ci configuration" +``` + +--- + +## Task 14: Add Quality Gates to CI Workflow + +**Files:** +- Modify: `.github/workflows/ci.yml` + +**Step 1: Add quality job** + +Add a new `quality` job to `.github/workflows/ci.yml`: + +```yaml + quality: + name: Quality Gates + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests with coverage + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + + - name: Build + run: npm run build + + - name: Check bundle size + run: npx bundlesize + + - name: Security audit + run: npm audit --audit-level=moderate + continue-on-error: true +``` + +**Step 2: Update lighthouse job** + +Update the existing `lighthouse` job to use the new config and enforce thresholds. + +**Step 3: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "feat(ci): add quality gates to ci workflow" +``` + +--- + +## Task 15: Create Gitleaks Configuration + +**Files:** +- Create: `.gitleaks.toml` + +**Step 1: Create gitleaks config** + +Create `.gitleaks.toml`: + +```toml +title = "Gitleaks Configuration" + +[extend] +useDefault = true + +[[allowlists]] +description = "Allowlisted files" +paths = [ + 'package-lock.json', + 'backend/package-lock.json' +] + +[[allowlists]] +description = "Test secrets" +regexes = [ + 'test.*password', + 'test.*secret', + 'test.*token' +] +``` + +**Step 2: Commit** + +```bash +git add .gitleaks.toml +git commit -m "feat(security): add gitleaks configuration" +``` + +--- + +## Task 16: Add Gitleaks to Pre-commit Hook + +**Files:** +- Modify: `.husky/pre-commit` + +**Step 1: Update pre-commit hook** + +Update `.husky/pre-commit` to: + +```bash +npm run lint + +# Gitleaks secret scanning +if command -v gitleaks &> /dev/null; then + gitleaks protect --verbose --staged + if [ $? -eq 1 ]; then + echo "" + echo "❌ Secrets detected in staged files!" + echo "Please remove sensitive data before committing." + echo "" + exit 1 + fi +fi +``` + +**Step 2: Commit** + +```bash +git add .husky/pre-commit +git commit -m "feat(security): add gitleaks to pre-commit hook" +``` + +--- + +## Task 17: Update Backend Tests for Knex + +**Files:** +- Modify: `backend/__tests__/api.test.js` + +**Step 1: Update tests to use async/await with Knex** + +Update the test file to work with the new async Knex-based API. + +**Step 2: Run tests** + +Run: `cd backend && npm test` +Expected: All tests pass + +**Step 3: Commit** + +```bash +git add backend/__tests__/ +git commit -m "test(api): update tests for knex" +``` + +--- + +## Task 18: Update Documentation + +**Files:** +- Modify: `README.md` + +**Step 1: Add gitleaks setup instructions** + +Add to README.md under a new "Development Setup" section: + +```markdown +### Pre-commit Setup + +This project uses Gitleaks for secret scanning. Install it before committing: + +\`\`\`bash +# macOS +brew install gitleaks + +# Linux +curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz | tar -xz +sudo mv gitleaks /usr/local/bin/ + +# Windows +scoop install gitleaks +\`\`\` + +### API Documentation + +API documentation is available at `/api/docs` when running the server. +``` + +**Step 2: Commit** + +```bash +git add README.md +git commit -m "docs(readme): add gitleaks setup and api docs info" +``` + +--- + +## Task 19: Verify All Configurations + +**Step 1: Run lint** + +```bash +npm run lint +``` + +Expected: No errors + +**Step 2: Run tests** + +```bash +npm run test:run +``` + +Expected: All tests pass + +**Step 3: Run backend tests** + +```bash +cd backend && npm test +``` + +Expected: All tests pass + +**Step 4: Test coverage** + +```bash +npm run test:coverage +``` + +Expected: Coverage report generated + +**Step 5: Build and check bundle** + +```bash +npm run build && npx bundlesize +``` + +Expected: Bundle sizes within limits + +--- + +## Task 20: Final Commit and Summary + +**Step 1: Verify all files are committed** + +```bash +git status +``` + +Expected: Nothing to commit + +**Step 2: View commit history** + +```bash +git log --oneline -20 +``` + +**Step 3: Summary** + +DevOps features implemented: +- **Swagger UI** at `/api/docs` with OpenAPI 3.0 spec +- **Knex migrations** with auto-migration on startup +- **Quality gates**: coverage, bundle size, npm audit, Lighthouse CI +- **Secret scanning**: Gitleaks pre-commit hook + +--- + +## Post-Implementation Checklist + +- [ ] Add `CODECOV_TOKEN` secret to GitHub +- [ ] Test Swagger UI at `/api/docs` +- [ ] Verify migrations run on startup +- [ ] Install gitleaks on developer machine +- [ ] Run full CI pipeline to verify quality gates \ No newline at end of file