15 Commits

Author SHA1 Message Date
Renovate Bot
22b25c05c4 chore(deps): update dependency @vitejs/plugin-react to v6
Some checks failed
renovate/artifacts Artifact file update failure
2026-03-13 00:00:47 +00:00
Renovate Bot
83fed13e56 Add renovate.json
Some checks failed
Release / Release (push) Has been cancelled
Release / Build & Push Docker Image (push) Has been cancelled
Stale Issues / stale (push) Successful in 2m59s
Nightly Build / Build Nightly Image (push) Has been skipped
2026-02-27 17:43:26 +00:00
semantic-release-bot
c18e4dda10 chore(release): 1.1.0 [skip ci]
## [1.1.0](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.4...v1.1.0) (2026-02-25)

### Features

* Add Helm chart for Kubernetes deployment ([#1](#1)) ([8deb3dc](8deb3dcd9f)), closes [CONTRIBUTING.md#commit-guidelines](https://git.seyshiro.de/tudattr/CONTRIBUTING.md/issues/commit-guidelines)
2026-02-25 05:04:33 +00:00
8deb3dcd9f feat: Add Helm chart for Kubernetes deployment (#1)
Some checks are pending
Stale Issues / stale (push) Successful in 1m10s
Release / Release (push) Successful in 3m32s
Release / Build & Push Docker Image (push) Has been skipped
Nightly Build / Build Nightly Image (push) Has started running
## Description

Add complete Helm chart for deploying the CV application to Kubernetes. The chart includes all necessary resources for a production-ready deployment with persistent storage and configurable authentication modes.

## Type of Change

- [x] `feat` - New feature (minor version bump)

## Breaking Changes

- [ ] This is a breaking change (major version bump)

**If breaking, describe the migration path:**

## Checklist

- [x] Commit messages follow [Conventional Commits](CONTRIBUTING.md#commit-guidelines)
- [x] Lint passes (`npm run lint`) - N/A for Helm charts
- [x] Build succeeds (`npm run build`) - N/A for Helm charts
- [x] Tests pass (`npm run test:run`) - N/A for Helm charts
- [ ] API documentation updated (if applicable)

## Testing

- `helm lint` passes on the chart
- `helm template test-release helm/cv-app/` renders 214 lines of valid Kubernetes manifests
- Verified all template helpers function correctly
- Tested conditional rendering (persistence enabled/disabled, simple/keycloak auth modes)

## Screenshots (if applicable)

N/A

## Related Issues

Implements the Helm chart design from `docs/plans/2026-02-23-helm-chart-design.md`

Co-authored-by: Tuan-Dat Tran <tuan-dat.tran@tudattr.dev>
Reviewed-on: #1
2026-02-24 22:27:45 +01:00
semantic-release-bot
8b5e12fa0a chore(release): 1.0.4 [skip ci]
## [1.0.4](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.3...v1.0.4) (2026-02-23)

### Bug Fixes

* **ci:** disable docker jobs due to missing docker in gitea runner ([bd44ea4](bd44ea42d7))

### Documentation

* add architecture documentation ([8d7000e](8d7000eb31))
* update all markdown documentation ([d73e876](d73e8769c0))
* update release-engineering documentation ([dd3b83f](dd3b83f7e1))
2026-02-23 22:04:22 +00:00
Tuan-Dat Tran
d73e8769c0 docs: update all markdown documentation
All checks were successful
Release / Release (push) Successful in 9m39s
Release / Build & Push Docker Image (push) Has been skipped
- README.md: Update tech stack versions, simplify content, add links to docs
- CONTRIBUTING.md: Add commit guidelines, testing instructions, PR process
- SECURITY.md: Add security measures, vulnerability reporting process
- PULL_REQUEST_TEMPLATE.md: Add conventional commit types, breaking change section
- Remove outdated plan documents (already implemented or superseded)
- architecture.md: Already updated with comprehensive system documentation
- release-engineering.md: Already updated with current pipeline status
2026-02-23 22:53:03 +01:00
Tuan-Dat Tran
8d7000eb31 docs: add architecture documentation
Some checks failed
Release / Build & Push Docker Image (push) Has been cancelled
Release / Release (push) Has been cancelled
- Document frontend and backend tech stacks
- Describe component hierarchy and routing
- Explain state management with React Context
- Document API endpoints and database schema
- Include authentication modes (simple/Keycloak)
- Describe design patterns used
- Document testing architecture
2026-02-23 22:46:46 +01:00
Tuan-Dat Tran
dd3b83f7e1 docs: update release-engineering documentation
Some checks failed
Release / Build & Push Docker Image (push) Has been cancelled
Release / Release (push) Has been cancelled
- Reflect Gitea Actions instead of GitHub Actions
- Document npm install optimization flags
- Add gitleaks to pre-commit hook documentation
- Update semantic-release config for Gitea compatibility
- Mark Docker workflows as temporarily disabled
- Update pipeline diagrams and environment table
2026-02-23 22:29:47 +01:00
Tuan-Dat Tran
bd44ea42d7 fix(ci): disable docker jobs due to missing docker in gitea runner
Gitea Actions runner uses node:18-bullseye container which lacks Docker.
Disable docker jobs until runner is configured with Docker-in-Docker
or a container with Docker pre-installed.
2026-02-23 22:24:24 +01:00
semantic-release-bot
ad2012e6d5 chore(release): 1.0.3 [skip ci]
## [1.0.3](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.2...v1.0.3) (2026-02-23)

### Performance Improvements

* **ci:** optimize npm install and fix deprecation warnings ([a593dd5](a593dd53c5))
2026-02-23 21:15:09 +00:00
Tuan-Dat Tran
a593dd53c5 perf(ci): optimize npm install and fix deprecation warnings
Some checks failed
Release / Release (push) Waiting to run
Release / Build & Push Docker Image (push) Has been cancelled
- Add --prefer-offline --no-audit --no-fund flags to npm ci
- Fix semantic-release deprecation: use successCommentCondition instead of successComment
2026-02-23 21:54:15 +01:00
semantic-release-bot
6f01124ac5 chore(release): 1.0.2 [skip ci]
## [1.0.2](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.1...v1.0.2) (2026-02-23)

### Bug Fixes

* **release:** disable github plugin comments for gitea compatibility ([de67a6d](de67a6d0ef))
2026-02-23 20:38:33 +00:00
Tuan-Dat Tran
de67a6d0ef fix(release): disable github plugin comments for gitea compatibility
Some checks failed
Release / Release (push) Successful in 21m15s
Release / Build & Push Docker Image (push) Failing after 7m16s
Gitea does not have GitHub's GraphQL API, causing the success/fail
comment features to fail. Disable these features while keeping
release creation functionality.
2026-02-23 21:05:21 +01:00
semantic-release-bot
af6fe5171f chore(release): 1.0.1 [skip ci]
## [1.0.1](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.0...v1.0.1) (2026-02-23)

### Bug Fixes

* **release:** remove dist assets from github release config ([e9d2667](e9d2667c95))
2026-02-23 19:16:01 +00:00
Tuan-Dat Tran
e9d2667c95 fix(release): remove dist assets from github release config
Some checks failed
Release / Release (push) Failing after 8m54s
Release / Build & Push Docker Image (push) Has been skipped
Gitea API fails when trying to upload directory assets. Since the project
deploys via Docker, the dist files are not needed in the release.
2026-02-23 20:03:33 +01:00
34 changed files with 2420 additions and 3203 deletions

View File

@@ -4,22 +4,37 @@
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] `feat` - New feature (minor version bump)
- [ ] `fix` - Bug fix (patch version bump)
- [ ] `perf` - Performance improvement (patch version bump)
- [ ] `docs` - Documentation only
- [ ] `refactor` - Code refactoring
- [ ] `test` - Adding/updating tests
- [ ] `ci` - CI/CD changes
- [ ] `chore` - Maintenance tasks
## Breaking Changes
- [ ] This is a breaking change (major version bump)
**If breaking, describe the migration path:**
## Checklist
- [ ] I have followed the [Contributing Guidelines](CONTRIBUTING.md)
- [ ] Commit messages follow [Conventional Commits](CONTRIBUTING.md#commit-guidelines)
- [ ] Lint passes (`npm run lint`)
- [ ] Build succeeds (`npm run build`)
- [ ] I have tested my changes locally
- [ ] Tests pass (`npm run test:run`)
- [ ] API documentation updated (if applicable)
## Testing
<!-- Describe how you tested these changes -->
## Screenshots (if applicable)
<!-- Add screenshots here -->
<!-- Add screenshots for UI changes -->
## Related Issues
<!-- Link any related issues: Closes #123 -->
<!-- Link issues: Closes #123, Fixes #456 -->

View File

@@ -22,7 +22,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Validate current commit
if: github.event_name == 'push'
@@ -50,7 +50,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Lint
run: npm run lint
@@ -82,7 +82,7 @@ jobs:
- name: Install dependencies
working-directory: ./backend
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Test
working-directory: ./backend
@@ -103,7 +103,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Run Integration Tests
run: npm run test:integration
@@ -123,7 +123,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
@@ -154,7 +154,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Run Regression Tests
run: npm run test:regression
@@ -182,7 +182,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Build
run: npm run build
@@ -206,7 +206,7 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Run tests with coverage
run: npm run test:coverage

View File

@@ -14,6 +14,7 @@ jobs:
name: Build Nightly Image
runs-on: ubuntu-latest
environment: nightly
if: false
steps:
- name: Checkout
@@ -32,11 +33,11 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Install backend dependencies
working-directory: ./backend
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Build
run: npm run build

View File

@@ -30,11 +30,11 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Install backend dependencies
working-directory: ./backend
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Lint
run: npm run lint
@@ -58,7 +58,7 @@ jobs:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: release
if: needs.release.result == 'success'
if: false
steps:
- name: Checkout

View File

@@ -14,6 +14,7 @@ jobs:
name: Build & Deploy to Staging
runs-on: ubuntu-latest
environment: staging
if: false
steps:
- name: Checkout
@@ -26,11 +27,11 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Install backend dependencies
working-directory: ./backend
run: npm ci
run: npm ci --prefer-offline --no-audit --no-fund
- name: Lint
run: npm run lint

3
.gitignore vendored
View File

@@ -23,6 +23,9 @@ dist-ssr
*.sln
*.sw?
# Worktrees
.worktrees/
# Database
backend/data/
*.db

View File

@@ -64,9 +64,9 @@
[
"@semantic-release/github",
{
"assets": [
{ "path": "dist/**/*", "label": "Distribution" }
]
"successCommentCondition": false,
"failCommentCondition": false,
"releasedLabels": false
}
]
]

View File

@@ -2,6 +2,42 @@
All notable changes to this project will be documented in this file.
## [1.1.0](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.4...v1.1.0) (2026-02-25)
### Features
* Add Helm chart for Kubernetes deployment ([#1](https://git.seyshiro.de/tudattr/kilo-cv/issues/1)) ([8deb3dc](https://git.seyshiro.de/tudattr/kilo-cv/commit/8deb3dcd9fcacfacd29ffe7b6dc7862ac9c48267)), closes [CONTRIBUTING.md#commit-guidelines](https://git.seyshiro.de/tudattr/CONTRIBUTING.md/issues/commit-guidelines)
## [1.0.4](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.3...v1.0.4) (2026-02-23)
### Bug Fixes
* **ci:** disable docker jobs due to missing docker in gitea runner ([bd44ea4](https://git.seyshiro.de/tudattr/kilo-cv/commit/bd44ea42d76fe8f177accf039da8a37975436748))
### Documentation
* add architecture documentation ([8d7000e](https://git.seyshiro.de/tudattr/kilo-cv/commit/8d7000eb31d0417ed3318111aa64160306915b3c))
* update all markdown documentation ([d73e876](https://git.seyshiro.de/tudattr/kilo-cv/commit/d73e8769c0207a248cf57898f2e20122c195a0b1))
* update release-engineering documentation ([dd3b83f](https://git.seyshiro.de/tudattr/kilo-cv/commit/dd3b83f7e11fc0680e491615957963ce7b9dd842))
## [1.0.3](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.2...v1.0.3) (2026-02-23)
### Performance Improvements
* **ci:** optimize npm install and fix deprecation warnings ([a593dd5](https://git.seyshiro.de/tudattr/kilo-cv/commit/a593dd53c5f9f7b170833c859025ff0aac2d17db))
## [1.0.2](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.1...v1.0.2) (2026-02-23)
### Bug Fixes
* **release:** disable github plugin comments for gitea compatibility ([de67a6d](https://git.seyshiro.de/tudattr/kilo-cv/commit/de67a6d0efc75baffd7f11f876427b5c5f48b229))
## [1.0.1](https://git.seyshiro.de/tudattr/kilo-cv/compare/v1.0.0...v1.0.1) (2026-02-23)
### Bug Fixes
* **release:** remove dist assets from github release config ([e9d2667](https://git.seyshiro.de/tudattr/kilo-cv/commit/e9d2667c959baf60c82519ecd5e24dac5953c4a0))
## 1.0.0 (2026-02-23)
### Features

View File

@@ -1,60 +1,164 @@
# Contributing to CV
# Contributing Guidelines
Thank you for your interest in contributing!
Thank you for your interest in contributing to this project! This document provides guidelines and instructions for contributing.
## Quick Start
## Code of Conduct
Be respectful and constructive. Treat all contributors with courtesy.
## Getting Started
### Prerequisites
- Node.js 20+
- npm 10+
- Gitleaks (for secret scanning)
### Setup
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run linting (`npm run lint`)
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
2. Clone your fork
3. Install dependencies:
## Development Guidelines
```bash
npm install
cd backend && npm install && cd ..
```
4. Install Gitleaks (for pre-commit hooks):
```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/
```
## Development Workflow
### Branch Naming
- `feat/feature-name` - New features
- `fix/bug-name` - Bug fixes
- `docs/topic` - Documentation changes
- `refactor/component` - Code refactoring
- `test/test-name` - Test additions/changes
### Commit Guidelines
This project uses [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <subject>
[optional body]
[optional footer]
```
#### Types
| Type | Description | Version Impact |
|------|-------------|----------------|
| `feat` | New feature | Minor |
| `fix` | Bug fix | Patch |
| `perf` | Performance improvement | Patch |
| `revert` | Revert previous commit | Patch |
| `docs` | Documentation only | None |
| `style` | Code style (formatting) | None |
| `refactor` | Code refactoring | None |
| `test` | Adding/updating tests | None |
| `build` | Build system changes | None |
| `ci` | CI/CD changes | None |
| `chore` | Maintenance tasks | None |
#### Scopes
| Scope | Description |
|-------|-------------|
| `admin` | Admin panel components |
| `api` | Backend API |
| `ui` | Frontend UI components |
| `docker` | Docker configuration |
| `ci` | CI/CD workflows |
| `deps` | Dependencies |
| `release` | Release configuration |
| `auth` | Authentication |
#### Examples
```bash
feat(admin): add export to PDF functionality
fix(api): resolve JWT token expiration issue
docs(readme): update installation instructions
ci(workflow): add staging deployment
```
### Pre-commit Hooks
The project uses Husky for git hooks:
- **pre-commit**: Runs ESLint and Gitleaks secret scanning
- **commit-msg**: Validates commit message format with commitlint
### Code Style
- Follow the existing code style
- ESLint configuration is enforced
- Run `npm run lint` before committing
- Use meaningful commit messages
- Fix issues with `npm run lint -- --fix`
### Component Structure
### Testing
```jsx
// Imports at top
import { motion } from 'framer-motion';
import { Icon } from 'lucide-react';
// Component definition
export default function Component() {
return (
// JSX
);
}
```
### Testing Your Changes
Run tests before submitting:
```bash
# Start dev server
npm run dev
# Unit tests
npm run test:run
# Build for production
npm run build
# Integration tests
npm run test:integration
# Preview production build
npm run preview
# E2E tests (requires running server)
npm run test:e2e
# All tests with coverage
npm run test:coverage
```
## Pull Request Checklist
## Pull Request Process
- [ ] Code follows the existing style
- [ ] Linting passes (`npm run lint`)
1. Create a feature branch from `master`
2. Make your changes following the guidelines above
3. Ensure all tests pass
4. Ensure lint passes (`npm run lint`)
5. Ensure build succeeds (`npm run build`)
6. Push to your fork and create a pull request
7. Fill out the PR template completely
8. Wait for review
### PR Checklist
- [ ] Follows contributing guidelines
- [ ] Commit messages follow conventional commits
- [ ] Lint passes (`npm run lint`)
- [ ] Tests pass (`npm run test:run`)
- [ ] Build succeeds (`npm run build`)
- [ ] Changes are documented in PR description
- [ ] Documentation updated if needed
## Release Process
Releases are automated via semantic-release:
1. Merge PR to `master`
2. CI runs tests and build
3. semantic-release analyzes commits
4. Version bumped, changelog updated
5. Git tag and release created
No manual release steps required.
## Questions?
Feel free to open an issue for any questions or discussions.
Open an issue for questions or discussions about contributions.

194
README.md
View File

@@ -1,8 +1,8 @@
# CV - Tuan-Dat Tran
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![React](https://img.shields.io/badge/React-18-61DAFB?logo=react)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.1-38B2AC?logo=tailwindcss)
![React](https://img.shields.io/badge/React-19-61DAFB?logo=react)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-4-38B2AC?logo=tailwindcss)
![Vite](https://img.shields.io/badge/Vite-7-646CFF?logo=vite)
A modern, minimalist CV/Resume single-page application with admin panel and persistent storage.
@@ -29,32 +29,33 @@ A modern, minimalist CV/Resume single-page application with admin panel and pers
- Modern, responsive design
- Smooth scroll animations with Framer Motion
- Admin panel for easy CV editing (password protected)
- Admin panel for CV editing (password protected)
- Persistent storage with SQLite
- Docker Compose deployment
- API-based architecture
- RESTful API with OpenAPI documentation
- Optional Keycloak integration for SSO
- Automated releases with semantic-release
## Tech Stack
### Frontend
- **React 18** - UI Library
- **Vite** - Build Tool
- **React 19** - UI Library
- **Vite 7** - Build Tool
- **Tailwind CSS 4** - Styling
- **Framer Motion** - Animations
- **Framer Motion 12** - Animations
- **Lucide React** - Icons
### Backend
- **Express.js** - API Server
- **SQLite** - Database
- **better-sqlite3** - SQLite driver
- **Express.js 4** - API Server
- **Knex.js** - SQL Query Builder
- **SQLite** with **better-sqlite3** - Database
## Getting Started
### Prerequisites
- Node.js 18+
- npm 9+
- Node.js 20+
- npm 10+
- Docker & Docker Compose (optional)
### Option 1: Docker Compose (Recommended)
@@ -66,6 +67,7 @@ docker-compose up -d
# Frontend: http://localhost:5173
# Backend API: http://localhost:3001
# Admin Panel: http://localhost:5173/admin
# API Docs: http://localhost:3001/api/docs
```
### Option 2: Local Development
@@ -86,27 +88,28 @@ npm run dev
### Environment Variables
Frontend (`.env`):
```
VITE_API_URL=http://localhost:3001
```
Backend (`backend/.env`):
```
PORT=3001
DB_PATH=./data/cv.db
USE_KEYCLOAK=false
# Keycloak settings (required if USE_KEYCLOAK=true)
AUTH_MODE=simple
# Keycloak settings (required if AUTH_MODE=keycloak)
KEYCLOAK_URL=https://keycloak.example.com
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=cv-admin
KEYCLOAK_CLIENT_ID=cv-app
```
## Development Setup
### Pre-commit Setup
This project uses Gitleaks for secret scanning. Install it before committing:
This project uses:
- **ESLint** for code linting
- **commitlint** for commit message validation
- **Husky** for git hooks
- **Gitleaks** for secret scanning
Install Gitleaks before committing:
```bash
# macOS
@@ -120,152 +123,41 @@ sudo mv gitleaks /usr/local/bin/
scoop install gitleaks
```
### API Documentation
### Commit Guidelines
API documentation is available at `/api/docs` when running the server.
Access the interactive Swagger UI at: `http://localhost:3001/api/docs`
## Admin Authentication
### Simple Mode (Default)
- A random password is generated and printed to the console on server startup
- Look for the "ADMIN PASSWORD" banner in the logs
- Enter this password to access the admin panel
### Keycloak Mode
Set `USE_KEYCLOAK=true` and configure Keycloak environment variables:
```bash
USE_KEYCLOAK=true
KEYCLOAK_URL=https://keycloak.example.com
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=cv-admin
```
## API Endpoints
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/cv` | No | Get CV data |
| PUT | `/api/cv` | Yes | Update CV data |
| GET | `/api/cv/export` | No | Export as JSON |
| POST | `/api/cv/import` | Yes | Import JSON data |
| GET | `/api/auth/config` | No | Get auth configuration |
| POST | `/api/auth/login` | No | Login with password |
| GET | `/api/docs` | No | Swagger API documentation |
| GET | `/health` | No | Health check |
## Project Structure
```
.
├── src/ # Frontend source
│ ├── components/ # CV components
│ ├── admin/ # Admin panel
│ ├── lib/ # Utilities
│ └── App.jsx
├── backend/ # Backend source
│ ├── routes/ # API routes
│ ├── db/ # Database
│ └── server.js
├── docker-compose.yml
├── Dockerfile
└── nginx.conf
```
## Testing
```bash
# Frontend tests
npm run test:run
# Backend tests
cd backend && npm test
```
## Release Process
This project uses [semantic-release](https://semantic-release.gitbook.io/) for automated versioning and releases.
### Conventional Commits
All commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
This project uses [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <description>
[optional body]
[optional footer]
# Examples:
feat(ui): add export button
fix(api): resolve authentication issue
docs(readme): update installation steps
```
#### Commit Types
### API Documentation
| Type | Description | Version Bump |
|------|-------------|--------------|
| `feat` | New feature | Minor |
| `fix` | Bug fix | Patch |
| `feat!` or `BREAKING CHANGE` | Breaking change | Major |
| `docs`, `style`, `refactor`, `test`, `build`, `ci`, `chore` | Non-release changes | None |
Interactive Swagger UI available at: `http://localhost:3001/api/docs`
#### Scopes
## Admin Authentication
Available scopes: `admin`, `api`, `ui`, `docker`, `ci`, `deps`, `release`, `auth`, `skills`, `experience`, `education`, `projects`, `personal`
### Simple Mode (Default)
- Random password generated on server startup
- Password displayed in console logs
- JWT token-based authentication
### Release Workflow
1. Push to `master` branch
2. CI runs tests and linting
3. semantic-release analyzes commits
4. If release needed:
- Version is bumped in package.json
- CHANGELOG.md is updated
- Git tag is created
- GitHub release is published
- Docker images are built and pushed
### Docker Images
Images are published to both Docker Hub and GitHub Container Registry:
| Tag | Description |
|-----|-------------|
| `latest` | Latest stable release |
| `v1.2.3` | Specific version |
| `staging` | Staging environment |
| `nightly`, `edge` | Daily builds from master |
| `YYYY-MM-DD` | Dated nightly build |
```bash
# Pull from Docker Hub
docker pull username/cv-app:latest
# Pull from GHCR
docker pull ghcr.io/owner/cv-app:latest
```
### Environments
| Environment | Branch | Trigger |
|-------------|--------|---------|
| Production | `master` | semantic-release |
| Staging | `staging` | Push to staging |
| Nightly | `master` | Daily at 02:00 UTC |
For detailed documentation, see [Release Engineering Documentation](docs/release-engineering.md).
### Keycloak Mode
- SSO integration via Keycloak
- Configure via `AUTH_MODE=keycloak` and related environment variables
## Documentation
| Document | Description |
|----------|-------------|
| [Release Engineering](docs/release-engineering.md) | Comprehensive release pipeline documentation |
| [Contributing](CONTRIBUTING.md) | Contribution guidelines |
| [Security](SECURITY.md) | Security policy |
## Contributing
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
| [Architecture](docs/architecture.md) | System architecture and design patterns |
| [Release Engineering](docs/release-engineering.md) | Release and deployment documentation |
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
MIT License - see [LICENSE](LICENSE) for details.

View File

@@ -4,33 +4,124 @@
| Version | Supported |
| ------- | ------------------ |
| main | :white_check_mark: |
| 1.x | :white_check_mark: |
| < 1.0 | :x: |
## Reporting a Vulnerability
We take security seriously. If you discover a security vulnerability, please follow these steps:
We take security seriously. If you discover a security vulnerability, please follow responsible disclosure.
1. **Do not** open a public issue
2. Email the maintainer directly at `tuan-dat.tran@example.com`
3. Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
### How to Report
### What to Expect
**Preferred Method**: Open a security advisory
- Acknowledgment within 48 hours
- Assessment within 7 days
- Fix timeline based on severity:
- Critical: 24-72 hours
- High: 1 week
- Medium/Low: Next release
1. Go to the repository
2. Click "Security" tab
3. Click "Report a vulnerability"
4. Fill out the advisory form
### Disclosure Policy
**Alternative**: Email the maintainer directly (if available in profile)
- Please allow time for the fix before public disclosure
### What to Include
- Description of the vulnerability
- Steps to reproduce
- Affected versions
- Potential impact
- Suggested fix (if available)
### Response Timeline
| Action | Timeline |
|--------|----------|
| Initial response | Within 48 hours |
| Vulnerability assessment | Within 7 days |
| Fix timeline based on severity | See below |
### Fix Timeline by Severity
| Severity | Timeline |
|----------|----------|
| Critical | 24-72 hours |
| High | 1 week |
| Medium | Next release |
| Low | Next release |
## Security Measures
This project implements several security measures:
### Code Quality
- ESLint for code analysis
- Automated testing before merge
- Code review required for all changes
### Secret Management
- Gitleaks pre-commit hook prevents committing secrets
- Environment variables for sensitive configuration
- No secrets in version control
### Authentication
- JWT tokens with configurable expiration
- Password hashed in database
- Token-based API authentication
### Dependencies
- Regular dependency audits via `npm audit`
- Dependabot for automated updates (if enabled)
## Best Practices for Contributors
### Do Not Commit
- API keys or tokens
- Database credentials
- JWT secrets
- Password files
- Private keys or certificates
### Environment Variables
Store sensitive configuration in environment variables:
```bash
# Backend
PORT=3001
DB_PATH=./data/cv.db
AUTH_MODE=simple
JWT_SECRET=your-secret-here
# Keycloak (if used)
KEYCLOAK_URL=https://keycloak.example.com
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=cv-app
```
### Running Security Audit
```bash
# Check for vulnerable dependencies
npm audit
# Fix vulnerabilities
npm audit fix
# Check backend dependencies
cd backend && npm audit
```
## Disclosure Policy
- Please allow reasonable time for the fix before public disclosure
- Coordinated disclosure is appreciated
- Credit will be given in the fix commit
- Credit will be given in the fix commit (if desired)
Thank you for helping keep this project secure!
## Contact
For security concerns, use the vulnerability reporting process above.
Thank you for helping keep this project secure!

448
docs/architecture.md Normal file
View File

@@ -0,0 +1,448 @@
<!-- markdownlint-disable MD013 -->
# Architecture Documentation
## Overview
kilo-cv is a full-stack web application for managing and displaying a professional CV/resume. It features a public-facing CV display page and an authenticated admin panel for content management.
### Key Features
- **Public CV Display**: Responsive, animated CV page with sections for experience, skills, education, and projects
- **Admin Panel**: Authenticated interface for editing CV content
- **REST API**: Backend API for data management
- **Export/Import**: JSON export and import functionality
- **API Documentation**: Interactive Swagger UI
## Technology Stack
### Frontend
| Technology | Version | Purpose |
|------------|---------|---------|
| React | 19.x | UI library |
| Vite | 7.x | Build tool and dev server |
| Tailwind CSS | 4.x | Styling |
| Framer Motion | 12.x | Animations |
| Lucide React | 0.574.x | Icon library |
| React Router DOM | 7.x | Client-side routing |
### Backend
| Technology | Version | Purpose |
|------------|---------|---------|
| Express.js | 4.x | REST API server |
| Knex.js | 3.x | SQL query builder |
| better-sqlite3 | 11.x | SQLite database driver |
| swagger-jsdoc | 6.x | OpenAPI documentation generation |
| swagger-ui-express | 5.x | Swagger UI serving |
## Directory Structure
```
kilo-cv/
├── src/ # Frontend source
│ ├── components/ # Public CV display components
│ │ ├── Hero.jsx # Name, title, introduction
│ │ ├── Experience.jsx # Work history section
│ │ ├── Skills.jsx # Skills by category
│ │ ├── Education.jsx # Education history
│ │ ├── Projects.jsx # Project showcase
│ │ └── Contact.jsx # Contact information
│ ├── admin/ # Admin panel
│ │ ├── AdminLayout.jsx # Admin layout with navigation
│ │ ├── AdminPersonal.jsx # Personal info editor
│ │ ├── AdminExperience.jsx # Experience editor
│ │ ├── AdminSkills.jsx # Skills editor
│ │ ├── AdminEducation.jsx # Education editor
│ │ ├── AdminProjects.jsx # Projects editor
│ │ ├── pages/
│ │ │ └── LoginPage.jsx # Authentication page
│ │ ├── sections/ # Reusable form components
│ │ │ ├── PersonalForm.jsx
│ │ │ ├── ExperienceForm.jsx
│ │ │ ├── SkillsForm.jsx
│ │ │ ├── EducationForm.jsx
│ │ │ └── ProjectsForm.jsx
│ │ ├── hooks/ # React contexts and custom hooks
│ │ │ ├── CVContext.jsx # CV data state management
│ │ │ ├── AuthContext.jsx # Authentication state
│ │ │ ├── useCVData.js # CV data access hook
│ │ │ └── useFormValidation.js
│ │ └── components/
│ │ └── ExportButton.jsx
│ ├── lib/ # Utilities and API client
│ │ ├── api.js # API client functions
│ │ ├── auth.js # Token management
│ │ ├── cv-data.js # CV validation utilities
│ │ └── cv.json # Default CV data structure
│ ├── data/
│ │ └── cv.js
│ ├── App.jsx # Root component with routing
│ ├── main.jsx # React entry point
│ └── index.css # Global styles (Tailwind)
├── backend/ # Backend source
│ ├── routes/ # API route handlers
│ │ ├── cv.js # CV CRUD endpoints
│ │ ├── auth.js # Authentication endpoints
│ │ └── docs.js # Swagger documentation route
│ ├── db/
│ │ └── init.js # Database initialization
│ ├── migrations/
│ │ └── 20260220000001_initial_schema.js
│ ├── middleware/
│ │ └── auth.js # JWT authentication middleware
│ ├── seeds/
│ │ └── initial_cv_data.js # Initial data seeding
│ ├── __tests__/
│ │ └── api.test.js # API tests
│ ├── server.js # Express server entry point
│ └── knexfile.js # Knex configuration
├── tests/ # Test suites
│ ├── e2e/ # Playwright E2E tests
│ ├── integration/ # Integration tests
│ ├── regression/ # Snapshot/regression tests
│ └── performance/ # k6 and Lighthouse tests
├── docs/ # Documentation
├── public/ # Static assets
└── .github/ # CI/CD workflows
```
## Frontend Architecture
### Routing Structure
```
/ → Public CV display
/admin → Admin panel (requires auth)
/admin/login → Login page
/admin/personal → Personal info editor
/admin/experience → Experience editor
/admin/skills → Skills editor
/admin/education → Education editor
/admin/projects → Projects editor
```
### Component Hierarchy
```
App
├── CVProvider (context)
│ └── AuthProvider (context)
│ ├── Routes
│ │ ├── "/" → CV Display Page
│ │ │ └── Hero, Experience, Skills, Education, Projects, Contact
│ │ └── "/admin/*" → AdminLayout
│ │ ├── LoginPage (unauthenticated)
│ │ └── Admin* Pages (authenticated)
│ │ └── *Form components
```
### State Management
The application uses React Context for state management:
**CVContext** (`src/admin/hooks/CVContext.jsx`)
- Manages CV data state
- Provides `useCVData()` hook for components
- Handles API communication for CRUD operations
**AuthContext** (`src/admin/hooks/AuthContext.jsx`)
- Manages authentication state
- Handles token storage and validation
- Provides `useAuth()` hook for protected routes
### Custom Hooks
| Hook | Purpose |
|------|---------|
| `useCVData()` | Access CV data and CRUD operations |
| `useAuth()` | Access authentication state and methods |
| `useFormValidation()` | Form validation with error handling |
### Styling
- **Tailwind CSS 4** with `@tailwindcss/vite` plugin
- Utility-first approach throughout
- Responsive design with mobile-first breakpoints
- Animations via Framer Motion
## Backend Architecture
### API Endpoints
| Endpoint | Method | Description | Auth |
|----------|--------|-------------|------|
| `/api/auth/config` | GET | Get auth configuration | No |
| `/api/auth/login` | POST | Authenticate with password | No |
| `/api/cv` | GET | Get CV data | No |
| `/api/cv` | PUT | Update CV data | Yes |
| `/api/cv/export` | GET | Export CV as JSON | No |
| `/api/cv/import` | POST | Import CV from JSON | Yes |
| `/api/docs` | GET | Swagger UI documentation | No |
| `/health` | GET | Health check endpoint | No |
### Request/Response Examples
**Get CV Data**
```http
GET /api/cv
Response: {
"personal": { "name": "...", "title": "...", ... },
"experience": [...],
"skills": {...},
"education": [...],
"projects": [...]
}
```
**Update CV Data**
```http
PUT /api/cv
Authorization: Bearer <token>
Body: { "personal": {...}, "experience": [...], ... }
Response: { "success": true }
```
**Login**
```http
POST /api/auth/login
Body: { "password": "..." }
Response: { "token": "..." }
```
### Database Schema
Single-table design for simplicity:
```sql
CREATE TABLE cv_data (
id INTEGER PRIMARY KEY CHECK (id = 1),
data TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
The CV data is stored as a JSON document in the `data` column:
```json
{
"personal": {
"name": "string",
"title": "string",
"intro": "string",
"email": "string",
"github": "string",
"linkedin": "string",
"location": "string"
},
"experience": [
{
"company": "string",
"position": "string",
"startDate": "YYYY-MM",
"endDate": "YYYY-MM or null",
"description": "string",
"highlights": ["string", ...]
}
],
"skills": {
"Category Name": ["skill1", "skill2", ...]
},
"education": [
{
"institution": "string",
"degree": "string",
"field": "string",
"startDate": "YYYY",
"endDate": "YYYY"
}
],
"projects": [
{
"name": "string",
"description": "string",
"technologies": ["tech1", ...],
"url": "string"
}
]
}
```
### Authentication
Two authentication modes are supported:
**Simple Mode** (default)
- Random password generated on server startup
- Password logged to console
- JWT token-based authentication
- Suitable for personal/single-user deployments
**Keycloak Mode** (optional)
- SSO integration via Keycloak
- Configure via environment variables
- Suitable for enterprise deployments
Environment variables for Keycloak:
```bash
AUTH_MODE=keycloak
KEYCLOAK_URL=https://keycloak.example.com
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=cv-app
```
### Middleware
**Auth Middleware** (`backend/middleware/auth.js`)
- Validates JWT token from Authorization header
- Attaches user info to request
- Returns 401 for invalid/missing tokens
## Design Patterns
### Frontend Patterns
1. **Context Provider Pattern**
- `CVProvider` and `AuthProvider` wrap the application
- Provides global state without prop drilling
2. **Custom Hook Pattern**
- `useCVData()`, `useAuth()` expose context values
- Encapsulates complex logic
3. **Container/Presentational Pattern**
- Admin pages fetch data from context (container)
- Form components handle UI (presentational)
4. **Form Validation Pattern**
- `useFormValidation` hook provides reusable validation
- Error state management standardized
### Backend Patterns
1. **Route Handler Pattern**
- Modular Express routers in `/routes`
- Clean separation of concerns
2. **Middleware Pattern**
- Auth middleware for protected routes
- Composable request processing
3. **Repository Pattern**
- `getDB()` returns Knex instance
- Database access abstracted from route handlers
4. **Migration-based Schema**
- Knex migrations for version control
- Rollback capability
## Testing Architecture
| Layer | Tool | Location | Purpose |
|-------|------|----------|---------|
| Unit | Vitest | `src/lib/__tests__/`, `src/admin/hooks/__tests__/` | Test utilities and hooks |
| API | Vitest + Supertest | `backend/__tests__/` | Test API endpoints |
| Integration | Vitest | `tests/integration/` | Test full-stack interactions |
| E2E | Playwright | `tests/e2e/` | Test user flows |
| Regression | Vitest | `tests/regression/` | Snapshot tests |
| Performance | k6, Lighthouse | `tests/performance/` | Load and performance testing |
### Test Commands
```bash
npm run test:run # Run unit tests
npm run test:coverage # Run tests with coverage
npm run test:integration # Run integration tests
npm run test:e2e # Run E2E tests (requires running server)
npm run test:regression # Run regression tests
```
## Configuration
### Frontend Configuration
**Vite** (`vite.config.js`)
- React plugin with SWC
- Tailwind CSS plugin
- Proxy configuration for API
**Tailwind** (`src/index.css`)
- Tailwind imports
- Custom CSS variables
### Backend Configuration
**Environment Variables**
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | 3001 | Server port |
| `AUTH_MODE` | simple | Authentication mode (simple/keycloak) |
| `KEYCLOAK_URL` | - | Keycloak server URL |
| `KEYCLOAK_REALM` | - | Keycloak realm |
| `KEYCLOAK_CLIENT_ID` | - | Keycloak client ID |
**Knex** (`backend/knexfile.js`)
- SQLite database configuration
- Migration and seed directories
## Development
### Prerequisites
- Node.js 20+
- npm 10+
### Getting Started
```bash
# Install frontend dependencies
npm install
# Install backend dependencies
cd backend && npm install
# Start backend server
npm run dev:backend
# In another terminal, start frontend
npm run dev
```
### Build
```bash
# Build frontend
npm run build
# Build outputs to dist/
```
### Docker
```bash
# Build and run with Docker Compose
docker-compose up --build
```
The Docker setup includes:
- Frontend container with nginx
- Backend container with Node.js
- SQLite volume for persistence
## Security Considerations
1. **Authentication**: JWT tokens with configurable expiration
2. **Secret Scanning**: Gitleaks pre-commit hook
3. **Input Validation**: Form validation on frontend, schema validation on backend
4. **CORS**: Configured for frontend origin
5. **SQL Injection**: Knex query builder prevents injection
## Future Considerations
- **Image Upload**: Profile picture support
- **PDF Export**: Generate PDF from CV data
- **Themes**: Customizable color schemes
- **Multi-language**: Internationalization support
- **Analytics**: View tracking for CV

View File

@@ -1,549 +0,0 @@
# DevOps Features Design
**Date:** 2026-02-20
**Status:** Approved
**Author:** Claude (AI Assistant)
## Overview
Implement four DevOps features for the CV application:
1. **Swagger/OpenAPI Documentation** - Interactive API docs at `/api/docs`
2. **Database Migrations** - Knex.js-based migration system with auto-migration
3. **Quality Gates** - Code coverage, bundle size, npm audit, Lighthouse CI
4. **Secret Scanning** - Gitleaks pre-commit hook
## 1. Swagger/OpenAPI Documentation
### Architecture
```
Route Files (JSDoc comments)
swagger-jsdoc
(generates spec at runtime)
swagger-ui-express
GET /api/docs → Interactive Swagger UI
GET /api/docs.json → Raw OpenAPI spec
```
### Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/docs` | Swagger UI interface |
| GET | `/api/docs.json` | Raw OpenAPI JSON spec |
### Implementation
**Dependencies:**
- `swagger-jsdoc` - Generate OpenAPI spec from JSDoc
- `swagger-ui-express` - Serve Swagger UI
**JSDoc format in route files:**
```javascript
/**
* @openapi
* /api/cv:
* get:
* summary: Get CV data
* tags: [CV]
* responses:
* 200:
* description: CV data
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CVData'
* 404:
* description: CV data not found
*/
router.get('/', (req, res) => { ... });
/**
* @openapi
* /api/cv:
* put:
* summary: Update CV data
* tags: [CV]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CVData'
* responses:
* 200:
* description: CV data updated
* 401:
* description: Unauthorized
*/
router.put('/', authMiddleware, (req, res) => { ... });
```
**Swagger setup:**
```javascript
// backend/routes/docs.js
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
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',
},
},
schemas: {
CVData: { /* schema definition */ },
},
},
},
apis: ['./routes/*.js'],
};
const specs = swaggerJsdoc(options);
router.use('/docs', swaggerUi.serve, swaggerUi.setup(specs));
router.get('/docs.json', (req, res) => res.json(specs));
```
---
## 2. Database Migrations (Knex.js)
### Architecture
```
backend/
├── knexfile.js # Knex configuration
├── migrations/
│ ├── 20260220000001_initial_schema.js
│ └── 20260220000002_add_auth_tables.js
├── seeds/
│ └── initial_cv_data.js
└── db/
├── connection.js # Knex instance
└── init.js # Auto-migration on startup
```
### Knex Configuration
```javascript
// knexfile.js
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
};
```
### Migration File Format
```javascript
// migrations/20260220000001_initial_schema.js
export async function up(knex) {
await knex.schema.createTable('cv_data', (table) => {
table.integer('id').primary().checkTo('=', 1);
table.text('data').notNullable();
table.datetime('updated_at').defaultTo(knex.fn.now());
});
}
export async function down(knex) {
await knex.schema.dropTableIfExists('cv_data');
}
```
### Auto-Migration on Startup
```javascript
// db/init.js
import knex from 'knex';
import config from '../knexfile.js';
let db = null;
export async function getDB() {
if (!db) {
db = knex(config);
await db.migrate.latest();
}
return db;
}
export async function closeDB() {
if (db) {
await db.destroy();
db = null;
}
}
```
### CLI Commands
```bash
# Create new migration
npx knex migrate:make migration_name --knexfile knexfile.js
# Run migrations manually
npx knex migrate:latest --knexfile knexfile.js
# Rollback last migration
npx knex migrate:rollback --knexfile knexfile.js
# Run seeds
npx knex seed:run --knexfile knexfile.js
```
### Dependencies
- `knex` - SQL query builder and migrations
---
## 3. Quality Gates
### Overview
| Gate | Tool | Threshold | Action |
|------|------|-----------|--------|
| Code Coverage | vitest + Codecov | 80% minimum | Fail CI |
| Bundle Size | bundlesize | 500KB max | Fail CI |
| npm audit | npm audit | Moderate+ | Fail CI |
| Lighthouse | @lhci/cli | Performance 90+ | Fail CI |
### CI Workflow Integration
```yaml
# .github/workflows/ci.yml additions
jobs:
quality:
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
# Code Coverage
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# Bundle Size
- name: Build
run: npm run build
- name: Check bundle size
run: npx bundlesize
# npm audit
- name: Security audit
run: npm audit --audit-level=moderate
lighthouse:
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: Build
run: npm run build
- name: Run Lighthouse CI
run: npm run test:lighthouse
```
### Configuration Files
**package.json:**
```json
{
"scripts": {
"test:coverage": "vitest run --coverage"
},
"bundlesize": [
{
"path": "./dist/assets/*.js",
"maxSize": "500kb"
},
{
"path": "./dist/assets/*.css",
"maxSize": "100kb"
}
]
}
```
**vitest.config.js:**
```javascript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
exclude: ['node_modules/', 'tests/'],
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
});
```
**.lighthouserc.json:**
```json
{
"ci": {
"collect": {
"url": ["http://localhost:4173/"],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["warn", { "minScore": 0.9 }],
"categories:best-practices": ["warn", { "minScore": 0.9 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
```
### Dependencies
- `@vitest/coverage-v8` - Coverage reporter
- `bundlesize` - Bundle size checking
### Secrets Required
| Secret | Description |
|--------|-------------|
| `CODECOV_TOKEN` | Codecov upload token |
---
## 4. Secret Scanning (Gitleaks)
### Architecture
```
Developer stages files
git commit
.husky/pre-commit
├─► npm run lint
└─► gitleaks protect --staged
Secrets found?
│ │
Yes No
│ │
▼ ▼
Block commit Allow commit
```
### Pre-commit Hook
```bash
# .husky/pre-commit
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
```
### Gitleaks Configuration
```toml
# .gitleaks.toml
title = "Gitleaks Configuration"
[extend]
useDefault = true
[[allowlists]]
description = "Allowlisted files"
paths = [
'package-lock.json',
'backend/package-lock.json',
'.*\\.md$'
]
[[allowlists]]
description = "Allowlisted patterns"
regexes = [
'VITE_API_URL.*localhost',
]
```
### Developer Setup
Gitleaks must be installed on developer machines:
```bash
# macOS
brew install gitleaks
# Linux
# Download from https://github.com/gitleaks/gitleaks/releases
# Or use go install
go install github.com/gitleaks/gitleaks/v8@latest
# Windows
scoop install gitleaks
# Or
choco install gitleaks
```
### Documentation
Add to README or CONTRIBUTING.md:
```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/download/v8.18.1/gitleaks_8.18.1_linux_x64.tar.gz | tar -xz
sudo mv gitleaks /usr/local/bin/
```
The pre-commit hook will automatically scan for secrets before each commit.
```
---
## Implementation Summary
### New Dependencies
**Backend:**
- `knex` - Database migrations and query builder
- `swagger-jsdoc` - OpenAPI spec generation
- `swagger-ui-express` - Swagger UI middleware
**Frontend:**
- `@vitest/coverage-v8` - Coverage reporting
- `bundlesize` - Bundle size checking
**Dev:**
- `gitleaks` (binary, not npm package)
### New Files
```
backend/
├── knexfile.js
├── migrations/
│ └── 20260220000001_initial_schema.js
├── seeds/
│ └── initial_cv_data.js
├── routes/
│ └── docs.js
└── db/
└── connection.js
.gitleaks.toml
.lighthouserc.json
```
### Modified Files
```
package.json # Add scripts, bundlesize config
vitest.config.js # Add coverage config
.github/workflows/ci.yml # Add quality gates
.husky/pre-commit # Add gitleaks
backend/db/init.js # Use Knex instead of better-sqlite3 directly
backend/routes/cv.js # Add JSDoc comments
backend/routes/auth.js # Add JSDoc comments
README.md # Add gitleaks setup instructions
```
---
## Post-Implementation Checklist
- [ ] Install dependencies
- [ ] Configure Knex and create initial migration
- [ ] Add Swagger UI route
- [ ] Add JSDoc comments to all API routes
- [ ] Configure Codecov (add CODECOV_TOKEN secret)
- [ ] Add bundlesize configuration
- [ ] Update CI workflow with quality gates
- [ ] Add gitleaks to pre-commit hook
- [ ] Update documentation

View File

@@ -1,948 +0,0 @@
# 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

View File

@@ -1,382 +0,0 @@
# Release Engineering Design
**Date:** 2026-02-20
**Status:** Approved
**Author:** Claude (AI Assistant)
## Overview
Implement a comprehensive release engineering pipeline for the CV application using semantic-release with Docker multi-registry publishing, automated changelog generation, and multi-environment deployment (production, staging, nightly).
## Requirements
- **Versioning:** Semantic Versioning with Conventional Commits
- **Changelog:** Automated from conventional commit messages
- **Release Trigger:** Continuous Delivery (automatic from main branch)
- **Deployment Targets:** Docker Hub + GitHub Container Registry (GHCR)
- **Environments:** Production, Staging, Nightly/Edge
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ GitHub Repository │
│ │
┌──────────┐ │ main ─────────────────────────────────────────────────► │ Production Release
│ Developer│───────►│ │ │ ├─ Git tag v1.0.0
└──────────┘ │ └─────► semantic-release │ ├─ GitHub Release
│ ├─ version bump │ ├─ Docker Hub: latest, v1.0.0
│ ├─ CHANGELOG.md │ └─ GHCR: latest, v1.0.0
│ ├─ git tag │
│ └─ GitHub release │
│ │
│ staging ───────► Deploy to staging environment │ Staging Deployment
│ (Docker tag: staging) │
│ │
│ nightly ────────► Build nightly from main │ Nightly/Edge Builds
│ (cron job) (Docker tag: nightly, edge) │
│ │
└─────────────────────────────────────────────────────────┘
```
## Versioning Strategy
### Semantic Versioning (SemVer)
Version format: `MAJOR.MINOR.PATCH` (e.g., `1.2.3`)
| Commit Type | Version Bump | Example |
|-------------|--------------|---------|
| `feat:` | Minor | 1.0.0 → 1.1.0 |
| `fix:` | Patch | 1.0.0 → 1.0.1 |
| `feat!:` or `BREAKING CHANGE:` | Major | 1.0.0 → 2.0.0 |
| `chore:`, `docs:`, `test:`, `style:`, `refactor:`, `perf:` | None | No release |
### Conventional Commits Format
```
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
```
Examples:
- `feat(admin): add password protection for admin panel`
- `fix(skills): prevent focus loss when editing category headers`
- `docs(readme): update installation instructions`
- `feat(api)!: remove deprecated endpoints`
### Commit Types
| Type | Description | Release? |
|------|-------------|----------|
| `feat` | New feature | Yes (minor) |
| `fix` | Bug fix | Yes (patch) |
| `docs` | Documentation changes | No |
| `style` | Code style changes (formatting) | No |
| `refactor` | Code refactoring | No |
| `perf` | Performance improvements | No |
| `test` | Adding/updating tests | No |
| `build` | Build system changes | No |
| `ci` | CI/CD changes | No |
| `chore` | Maintenance tasks | No |
| `revert` | Revert previous commit | Yes (if reverted commit was releasing) |
## Release Pipeline
### Production Release (main branch)
1. Developer pushes/merges to `main`
2. CI workflow runs (lint, test, build, e2e, integration)
3. semantic-release analyzes commits since last release
4. If release-worthy commits found:
- Calculates new version
- Updates `package.json` version
- Generates/updates `CHANGELOG.md`
- Creates git tag (e.g., `v1.0.0`)
- Creates GitHub release with release notes
- Builds Docker image
- Pushes to Docker Hub and GHCR with version tags
### Staging Deployment
1. Push to `staging` branch
2. CI workflow runs (lint, test, build)
3. Build Docker image
4. Push to registries with `staging` tag
5. Deploy to staging environment
### Nightly Builds
1. Scheduled workflow runs at 02:00 UTC daily
2. Checkout `main` branch
3. Build Docker image
4. Push to registries with tags:
- `nightly`
- `edge`
- `YYYY-MM-DD` (e.g., `2026-02-20`)
## Docker Registry Publishing
### Registry Configuration
**Docker Hub:**
- Image: `DOCKERHUB_USERNAME/cv-app`
- Requires: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` secrets
**GitHub Container Registry (GHCR):**
- Image: `ghcr.io/GITHUB_OWNER/cv-app`
- Requires: `GITHUB_TOKEN` (automatic)
### Tag Strategy
| Event | Docker Hub Tags | GHCR Tags |
|-------|-----------------|-----------|
| Production release v1.2.3 | `latest`, `v1.2.3`, `1.2`, `1` | `latest`, `v1.2.3`, `1.2`, `1` |
| Staging push | `staging` | `staging` |
| Nightly build | `nightly`, `edge`, `2026-02-20` | `nightly`, `edge`, `2026-02-20` |
### Multi-architecture Support
Build for multiple platforms:
- `linux/amd64` (x86_64)
- `linux/arm64` (Apple Silicon, AWS Graviton)
## Changelog Generation
### Format
```markdown
## [1.1.0] - 2026-02-20
### Features
* **admin:** add password protection for admin panel (#123) ([abc1234](https://github.com/owner/repo/commit/abc1234))
* **api:** add authentication endpoints (#120) ([def5678](https://github.com/owner/repo/commit/def5678))
### Bug Fixes
* **skills:** prevent focus loss when editing category headers (#124) ([ghi9012](https://github.com/owner/repo/commit/ghi9012))
### Documentation
* update installation instructions (#125) ([jkl3456](https://github.com/owner/repo/commit/jkl3456))
### BREAKING CHANGES
* **api:** remove deprecated `/api/v1/*` endpoints
---
## [1.0.0] - 2026-02-15
...
```
### Tools
- `@semantic-release/changelog` - Generates CHANGELOG.md
- `@semantic-release/git` - Commits updated files back to repo
- `conventional-changelog-conventionalcommits` - Preset for conventional commits
## GitHub Environments
### Production
- **Protection:** Required reviewers (1+)
- **Deploy Trigger:** semantic-release on main branch merge
- **Secrets:** `DOCKERHUB_TOKEN`, `DOCKERHUB_USERNAME`
- **URL:** Production deployment URL
### Staging
- **Protection:** Optional reviewers
- **Deploy Trigger:** Push to `staging` branch
- **URL:** Staging deployment URL
### Nightly
- **Protection:** None
- **Deploy Trigger:** Scheduled (cron: `0 2 * * *`)
- **URL:** Nightly build URL
## Branch Protection Rules
### main branch
| Rule | Setting |
|------|---------|
| Require PR reviews | 1+ |
| Dismiss stale reviews | Yes |
| Require linear history | Yes |
| Restrict force pushes | Yes |
| Allow deletions | No |
| Required status checks | `frontend`, `backend`, `integration`, `e2e` |
### staging branch
| Rule | Setting |
|------|---------|
| Require PR reviews | No |
| Require linear history | No |
| Restrict force pushes | No |
| Required status checks | `frontend`, `backend` |
## File Structure
```
.github/
├── workflows/
│ ├── ci.yml # Enhanced with commitlint
│ ├── release.yml # semantic-release workflow
│ ├── staging.yml # Staging deployment
│ └── nightly.yml # Nightly builds
├── commitlint.config.js # Conventional commits config
└── .releaserc.json # semantic-release config
scripts/
├── docker-tags.sh # Docker tag computation
└── release-notes.sh # Custom release notes
docker/
├── Dockerfile # Multi-stage build
└── docker-compose.prod.yml # Production compose file
CHANGELOG.md # Generated by semantic-release
package.json # Version updated by semantic-release
```
## Implementation Components
### 1. semantic-release Configuration
`.releaserc.json`:
```json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git",
"@semantic-release/github"
]
}
```
### 2. commitlint Configuration
`commitlint.config.js`:
```javascript
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'build', 'ci', 'chore', 'revert'
]],
'scope-enum': [2, 'always', [
'admin', 'api', 'ui', 'docker', 'ci', 'deps'
]]
}
};
```
### 3. Husky Git Hooks
```bash
npx husky init
echo "npx commitlint --edit \$1" > .husky/commit-msg
```
### 4. GitHub Workflows
**release.yml:**
- Triggers on push to main
- Runs semantic-release
- Builds and pushes Docker images
**staging.yml:**
- Triggers on push to staging branch
- Builds and pushes Docker images with staging tag
**nightly.yml:**
- Triggers on schedule (daily at 02:00 UTC)
- Builds from main with nightly/edge tags
### 5. Docker Configuration
Multi-stage Dockerfile with:
- Build stage for frontend
- Build stage for backend
- Production stage with nginx
Multi-platform build using `docker buildx`
## Security Considerations
1. **Secrets Management:**
- Use GitHub Secrets for sensitive values
- Never commit secrets to repository
- Use `GITHUB_TOKEN` for GHCR (automatic)
2. **Branch Protection:**
- Require PR reviews for main
- Require status checks to pass
- Restrict force pushes
3. **Docker Image Security:**
- Use minimal base images (alpine)
- Run as non-root user
- Scan images for vulnerabilities (optional: Trivy)
4. **Environment Protection:**
- Required reviewers for production
- Environment-specific secrets
## Migration Plan
1. **Phase 1: Setup Tooling**
- Install semantic-release and plugins
- Configure commitlint and husky
- Create .releaserc.json
2. **Phase 2: Update Workflows**
- Enhance ci.yml with commitlint
- Create release.yml
- Create staging.yml
- Create nightly.yml
3. **Phase 3: Docker Publishing**
- Configure Docker Hub credentials
- Update Dockerfile for multi-platform
- Test image publishing
4. **Phase 4: Branch Protection**
- Create staging branch
- Configure branch protection rules
- Create GitHub environments
5. **Phase 5: First Release**
- Make a conventional commit
- Verify semantic-release runs
- Check GitHub release created
- Verify Docker images published
## Success Criteria
- [ ] Conventional commits enforced via commitlint
- [ ] Version automatically bumped based on commit types
- [ ] CHANGELOG.md automatically generated and updated
- [ ] Git tags created for each release
- [ ] GitHub releases created with release notes
- [ ] Docker images published to Docker Hub and GHCR
- [ ] Multi-platform Docker images (amd64, arm64)
- [ ] Staging deployments on staging branch push
- [ ] Nightly builds running daily
- [ ] Branch protection rules enforced
## References
- [semantic-release Documentation](https://semantic-release.gitbook.io/)
- [Conventional Commits Specification](https://www.conventionalcommits.org/)
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [Docker Multi-platform Builds](https://docs.docker.com/build/building/multi-platform/)

View File

@@ -1,974 +0,0 @@
# Release Engineering Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Implement comprehensive release engineering with semantic-release, Docker multi-registry publishing, and multi-environment deployment (production, staging, nightly).
**Architecture:** semantic-release handles versioning, changelog, and GitHub releases from conventional commits. GitHub Actions workflows build and publish Docker images to Docker Hub and GHCR. Separate workflows for staging and nightly deployments.
**Tech Stack:** semantic-release, commitlint, husky, Docker buildx, GitHub Actions
---
## Task 1: Install Release Engineering Dependencies
**Files:**
- Modify: `package.json`
- Create: `.releaserc.json`
- Create: `commitlint.config.js`
**Step 1: Install semantic-release and plugins**
```bash
npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/github @commitlint/cli @commitlint/config-conventional husky
```
**Step 2: Verify installation**
Run: `npm list semantic-release @commitlint/cli husky --depth=0`
Expected: All packages listed with versions
**Step 3: Commit**
```bash
git add package.json package-lock.json
git commit -m "build: add semantic-release, commitlint, and husky dependencies"
```
---
## Task 2: Configure semantic-release
**Files:**
- Create: `.releaserc.json`
**Step 1: Create semantic-release configuration file**
Create `.releaserc.json`:
```json
{
"branches": ["main", { "name": "staging", "prerelease": true }],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "revert", "release": "patch" },
{ "type": "docs", "release": false },
{ "type": "style", "release": false },
{ "type": "refactor", "release": false },
{ "type": "test", "release": false },
{ "type": "build", "release": false },
{ "type": "ci", "release": false },
{ "type": "chore", "release": false }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Styles", "hidden": true },
{ "type": "refactor", "section": "Code Refactoring", "hidden": true },
{ "type": "test", "section": "Tests", "hidden": true },
{ "type": "build", "section": "Build System", "hidden": true },
{ "type": "ci", "section": "Continuous Integration", "hidden": true },
{ "type": "chore", "section": "Chores", "hidden": true }
]
}
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md",
"changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file."
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["package.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github",
{
"assets": [
{ "path": "dist/**/*", "label": "Distribution" }
]
}
]
]
}
```
**Step 2: Verify JSON is valid**
Run: `node -e "console.log(JSON.parse(require('fs').readFileSync('.releaserc.json', 'utf8')))"`
Expected: JSON object printed without errors
**Step 3: Commit**
```bash
git add .releaserc.json
git commit -m "chore(ci): configure semantic-release"
```
---
## Task 3: Configure commitlint
**Files:**
- Create: `commitlint.config.js`
**Step 1: Create commitlint configuration file**
Create `commitlint.config.js`:
```javascript
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'revert',
],
],
'scope-enum': [
2,
'always',
[
'admin',
'api',
'ui',
'docker',
'ci',
'deps',
'release',
'auth',
'skills',
'experience',
'education',
'projects',
'personal',
],
],
'scope-empty': [1, 'never'],
'subject-case': [2, 'always', 'lower-case'],
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [2, 'always', 100],
},
};
```
**Step 2: Verify configuration is valid**
Run: `npx commitlint --from HEAD~1 --to HEAD --verbose` (skip if only one commit)
Expected: Either passes or reports linting issues
**Step 3: Commit**
```bash
git add commitlint.config.js
git commit -m "chore(ci): configure commitlint for conventional commits"
```
---
## Task 4: Configure Husky Git Hooks
**Files:**
- Create: `.husky/commit-msg`
- Create: `.husky/pre-commit`
**Step 1: Initialize husky**
```bash
npx husky init
```
Expected: `.husky/` directory created
**Step 2: Create commit-msg hook**
Replace `.husky/commit-msg` content with:
```bash
npx --no -- commitlint --edit "$1"
```
**Step 3: Create pre-commit hook**
Replace `.husky/pre-commit` content with:
```bash
npm run lint
```
**Step 4: Make hooks executable**
```bash
chmod +x .husky/pre-commit .husky/commit-msg
```
**Step 5: Verify husky is configured in package.json**
Run: `cat package.json | grep -A 2 '"scripts"'`
Expected: `prepare` script present with `husky`
If not present, add to package.json scripts:
```json
"prepare": "husky"
```
**Step 6: Commit**
```bash
git add .husky package.json
git commit -m "chore(ci): configure husky git hooks for commitlint"
```
---
## Task 5: Create Docker Multi-Platform Configuration
**Files:**
- Modify: `Dockerfile`
- Create: `docker bake.hcl`
**Step 1: Update Dockerfile for multi-platform support**
Read current `Dockerfile` and ensure it supports multi-platform. The existing Dockerfile should already work, but verify:
- No hardcoded architecture-specific paths
- Uses multi-stage build
- Backend builds better-sqlite3 from source
**Step 2: Create docker-bake.hcl for multi-registry publishing**
Create `docker-bake.hcl`:
```hcl
variable "REGISTRY_USER" {
default = ""
}
variable "IMAGE_NAME" {
default = "cv-app"
}
variable "TAG" {
default = "latest"
}
variable "VERSION" {
default = ""
}
group "default" {
targets = ["cv-app"]
}
target "cv-app" {
context = "."
platforms = ["linux/amd64", "linux/arm64"]
tags = [
notequal("",VERSION) ? "${REGISTRY_USER}/${IMAGE_NAME}:${VERSION}" : "",
notequal("",VERSION) ? "${REGISTRY_USER}/${IMAGE_NAME}:latest" : "",
notequal("",VERSION) ? "ghcr.io/${REGISTRY_USER}/${IMAGE_NAME}:${VERSION}" : "",
notequal("",VERSION) ? "ghcr.io/${REGISTRY_USER}/${IMAGE_NAME}:latest" : "",
]
args = {
NODE_VERSION = "20"
}
}
```
**Step 3: Commit**
```bash
git add Dockerfile docker-bake.hcl
git commit -m "feat(docker): add multi-platform build configuration"
```
---
## Task 6: Create Release Workflow
**Files:**
- Modify: `.github/workflows/release.yml`
**Step 1: Replace existing release.yml**
Replace `.github/workflows/release.yml` content with:
```yaml
name: Release
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
packages: write
id-token: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install backend dependencies
working-directory: ./backend
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test:run
- name: Run backend tests
working-directory: ./backend
run: npm test
- name: Build
run: npm run build
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: release
if: needs.release.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version
id: version
run: |
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "latest")
echo "version=${VERSION#v}" >> $GITHUB_OUTPUT
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/cv-app:latest
${{ secrets.DOCKERHUB_USERNAME }}/cv-app:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository_owner }}/cv-app:latest
ghcr.io/${{ github.repository_owner }}/cv-app:${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
```
**Step 2: Verify YAML syntax**
Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))"`
Expected: No errors
**Step 3: Commit**
```bash
git add .github/workflows/release.yml
git commit -m "feat(ci): add semantic-release workflow with Docker publishing"
```
---
## Task 7: Create Staging Workflow
**Files:**
- Create: `.github/workflows/staging.yml`
**Step 1: Create staging workflow**
Create `.github/workflows/staging.yml`:
```yaml
name: Staging Deploy
on:
push:
branches: [staging]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build-and-deploy:
name: Build & Deploy to Staging
runs-on: ubuntu-latest
environment: staging
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: Install backend dependencies
working-directory: ./backend
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test:run
- name: Build
run: npm run build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/cv-app:staging
ghcr.io/${{ github.repository_owner }}/cv-app:staging
cache-from: type=gha
cache-to: type=gha,mode=max
```
**Step 2: Verify YAML syntax**
Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/staging.yml'))"`
Expected: No errors
**Step 3: Commit**
```bash
git add .github/workflows/staging.yml
git commit -m "feat(ci): add staging deployment workflow"
```
---
## Task 8: Create Nightly Build Workflow
**Files:**
- Create: `.github/workflows/nightly.yml`
**Step 1: Create nightly workflow**
Create `.github/workflows/nightly.yml`:
```yaml
name: Nightly Build
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
nightly:
name: Build Nightly Image
runs-on: ubuntu-latest
environment: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
- name: Get date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install backend dependencies
working-directory: ./backend
run: npm ci
- name: Build
run: npm run build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/cv-app:nightly
${{ secrets.DOCKERHUB_USERNAME }}/cv-app:edge
${{ secrets.DOCKERHUB_USERNAME }}/cv-app:${{ steps.date.outputs.date }}
ghcr.io/${{ github.repository_owner }}/cv-app:nightly
ghcr.io/${{ github.repository_owner }}/cv-app:edge
ghcr.io/${{ github.repository_owner }}/cv-app:${{ steps.date.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
```
**Step 2: Verify YAML syntax**
Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/nightly.yml'))"`
Expected: No errors
**Step 3: Commit**
```bash
git add .github/workflows/nightly.yml
git commit -m "feat(ci): add nightly build workflow"
```
---
## Task 9: Enhance CI Workflow with Commitlint
**Files:**
- Modify: `.github/workflows/ci.yml`
**Step 1: Add commitlint job to CI**
Add this job to `.github/workflows/ci.yml` after the `jobs:` line:
```yaml
commitlint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate current commit
if: github.event_name == 'push'
run: npx commitlint --from HEAD~1 --to HEAD --verbose
- name: Validate PR commits
if: github.event_name == 'pull_request'
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
```
**Step 2: Update job dependencies**
Add `commitlint` to the `needs` array of downstream jobs. For example, change:
```yaml
integration:
runs-on: ubuntu-latest
needs: [frontend, backend]
```
to:
```yaml
integration:
runs-on: ubuntu-latest
needs: [frontend, backend, commitlint]
```
Do the same for `e2e` and `regression` jobs.
**Step 3: Verify YAML syntax**
Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"`
Expected: No errors
**Step 4: Commit**
```bash
git add .github/workflows/ci.yml
git commit -m "feat(ci): add commitlint validation to CI workflow"
```
---
## Task 10: Create CHANGELOG.md
**Files:**
- Create: `CHANGELOG.md`
**Step 1: Create initial CHANGELOG**
Create `CHANGELOG.md`:
```markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Release engineering with semantic-release
- Multi-platform Docker builds (amd64, arm64)
- Docker Hub and GHCR publishing
- Staging and nightly deployment workflows
- Commitlint for conventional commits
## [0.0.0] - 2026-02-20
### Added
- Initial CV application
- React frontend with Tailwind CSS v4
- Express.js backend with SQLite
- Admin panel for CV editing
- Password protection for admin
- Docker Compose deployment
```
**Step 2: Commit**
```bash
git add CHANGELOG.md
git commit -m "docs: add initial CHANGELOG.md"
```
---
## Task 11: Update README with Release Information
**Files:**
- Modify: `README.md`
**Step 1: Add release engineering section to README**
Add this section after the "Testing" section:
```markdown
## Release Process
This project uses [semantic-release](https://semantic-release.gitbook.io/) for automated versioning and releases.
### Conventional Commits
All commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
#### Commit Types
| Type | Description | Version Bump |
|------|-------------|--------------|
| `feat` | New feature | Minor |
| `fix` | Bug fix | Patch |
| `feat!` or `BREAKING CHANGE` | Breaking change | Major |
| `docs`, `style`, `refactor`, `test`, `build`, `ci`, `chore` | Non-release changes | None |
#### Scopes
Available scopes: `admin`, `api`, `ui`, `docker`, `ci`, `deps`, `release`, `auth`, `skills`, `experience`, `education`, `projects`, `personal`
### Release Workflow
1. Push to `main` branch
2. CI runs tests and linting
3. semantic-release analyzes commits
4. If release needed:
- Version is bumped in package.json
- CHANGELOG.md is updated
- Git tag is created
- GitHub release is published
- Docker images are built and pushed
### Docker Images
Images are published to both Docker Hub and GitHub Container Registry:
| Tag | Description |
|-----|-------------|
| `latest` | Latest stable release |
| `v1.2.3` | Specific version |
| `staging` | Staging environment |
| `nightly`, `edge` | Daily builds from main |
| `YYYY-MM-DD` | Dated nightly build |
```bash
# Pull from Docker Hub
docker pull username/cv-app:latest
# Pull from GHCR
docker pull ghcr.io/owner/cv-app:latest
```
### Environments
| Environment | Branch | Trigger |
|-------------|--------|---------|
| Production | `main` | semantic-release |
| Staging | `staging` | Push to staging |
| Nightly | `main` | Daily at 02:00 UTC |
```
**Step 2: Commit**
```bash
git add README.md
git commit -m "docs: add release process documentation"
```
---
## Task 12: Update package.json Version
**Files:**
- Modify: `package.json`
**Step 1: Update version field**
Change `"version": "0.0.0"` to `"version": "0.0.0-dev.1"` in package.json.
This indicates the project is in development before the first release. semantic-release will set the proper version on first release.
**Step 2: Commit**
```bash
git add package.json
git commit -m "chore: set initial development version"
```
---
## Task 13: 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: Test commitlint locally**
```bash
echo "feat(test): test commit message" | npx commitlint
```
Expected: No errors
```bash
echo "invalid commit message" | npx commitlint
```
Expected: Error message about invalid format
**Step 4: Verify husky hooks**
```bash
ls -la .husky/
```
Expected: `pre-commit` and `commit-msg` files exist and are executable
---
## Task 14: Final Commit and Summary
**Step 1: Verify all files are committed**
```bash
git status
```
Expected: "nothing to commit, working tree clean" or only untracked files that shouldn't be committed
**Step 2: View commit history**
```bash
git log --oneline -15
```
Expected: All 12+ commits from this plan visible
**Step 3: Summary**
The release engineering system is now configured:
- **semantic-release** handles automated versioning and releases
- **commitlint** enforces conventional commits
- **husky** provides git hooks for validation
- **GitHub Actions** workflows for release, staging, and nightly builds
- **Docker** multi-platform images published to Docker Hub and GHCR
---
## Required Secrets
Before the first release, configure these secrets in GitHub repository settings:
| Secret | Description |
|--------|-------------|
| `DOCKERHUB_USERNAME` | Docker Hub username |
| `DOCKERHUB_TOKEN` | Docker Hub access token |
| `GITHUB_TOKEN` | Automatic (no configuration needed) |
---
## Post-Implementation Checklist
- [ ] Configure Docker Hub secrets in GitHub
- [ ] Create `staging` branch
- [ ] Configure GitHub environments (production, staging, nightly)
- [ ] Set up branch protection rules for `main`
- [ ] Test release by merging a `feat:` commit to main

View File

@@ -0,0 +1,491 @@
# Helm Chart Design
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add Helm chart for Kubernetes deployment as an alternative to Docker Compose.
**Architecture:** Single Helm chart deploying frontend (nginx) and backend (Node.js) services with PVC for SQLite persistence and Ingress for external access.
**Tech Stack:** Helm 3, Kubernetes
---
## Chart Structure
```
helm/cv-app/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── _helpers.tpl
│ ├── frontend-deployment.yaml
│ ├── frontend-service.yaml
│ ├── backend-deployment.yaml
│ ├── backend-service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── pvc.yaml
│ └── NOTES.txt
└── .helmignore
```
## Components
### Frontend
- **Deployment**: nginx serving built React static files
- **Service**: ClusterIP on port 80
- **Environment**: API URL from ConfigMap
### Backend
- **Deployment**: Node.js Express server
- **Service**: ClusterIP on port 3001
- **Volume**: PVC mount at `/app/data` for SQLite
- **Environment**: Port, DB path, auth config from ConfigMap/Secret
### Ingress
- Routes `/` → frontend service
- Routes `/api` → backend service
- Configurable TLS
- Supports nginx, traefik ingress controllers
### Persistence
- PVC for SQLite database
- Configurable storage class
- 1Gi default size
## Values Schema
```yaml
frontend:
replicaCount: 1
image:
repository: username/cv-app
tag: latest
pullPolicy: IfNotPresent
resources: {}
backend:
replicaCount: 1
image:
repository: username/cv-app-backend
tag: latest
pullPolicy: IfNotPresent
auth:
mode: simple
keycloak:
url: ""
realm: ""
clientId: ""
resources: {}
persistence:
enabled: true
size: 1Gi
storageClass: ""
ingress:
enabled: true
className: ""
annotations: {}
hosts:
- host: cv.local
paths:
- path: /
service: frontend
- path: /api
service: backend
tls: []
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
nodeSelector: {}
tolerations: []
affinity: {}
```
## Templates
### _helpers.tpl
```yaml
{{- define "cv-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cv-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "cv-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cv-app.labels" -}}
helm.sh/chart: {{ include "cv-app.chart" . }}
{{ include "cv-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "cv-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cv-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "cv-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "cv-app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
```
### frontend-deployment.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cv-app.fullname" . }}-frontend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
{{- include "cv-app.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: frontend
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "cv-app.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: frontend
spec:
containers:
- name: frontend
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "cv-app.fullname" . }}-config
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.frontend.resources | nindent 12 }}
```
### frontend-service.yaml
```yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "cv-app.fullname" . }}-frontend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
{{- include "cv-app.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: frontend
```
### backend-deployment.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cv-app.fullname" . }}-backend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
replicas: {{ .Values.backend.replicaCount }}
selector:
matchLabels:
{{- include "cv-app.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
labels:
{{- include "cv-app.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: backend
spec:
containers:
- name: backend
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
- name: http
containerPort: 3001
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "cv-app.fullname" . }}-config
- secretRef:
name: {{ include "cv-app.fullname" . }}-secret
volumeMounts:
- name: data
mountPath: /app/data
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
resources:
{{- toYaml .Values.backend.resources | nindent 12 }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "cv-app.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
```
### backend-service.yaml
```yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "cv-app.fullname" . }}-backend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
type: ClusterIP
ports:
- port: 3001
targetPort: http
protocol: TCP
name: http
selector:
{{- include "cv-app.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: backend
```
### ingress.yaml
```yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "cv-app.fullname" . }}
labels:
{{- include "cv-app.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: Prefix
backend:
service:
name: {{ include "cv-app.fullname" $ }}-{{ .service }}
port:
number: {{ if eq .service "frontend" }}80{{ else }}3001{{ end }}
{{- end }}
{{- end }}
{{- end }}
```
### configmap.yaml
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "cv-app.fullname" . }}-config
labels:
{{- include "cv-app.labels" . | nindent 4 }}
data:
PORT: "3001"
DB_PATH: "/app/data/cv.db"
AUTH_MODE: {{ .Values.backend.auth.mode | quote }}
{{- if eq .Values.backend.auth.mode "keycloak" }}
KEYCLOAK_URL: {{ .Values.backend.keycloak.url | quote }}
KEYCLOAK_REALM: {{ .Values.backend.keycloak.realm | quote }}
KEYCLOAK_CLIENT_ID: {{ .Values.backend.keycloak.clientId | quote }}
{{- end }}
```
### secret.yaml
```yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "cv-app.fullname" . }}-secret
labels:
{{- include "cv-app.labels" . | nindent 4 }}
type: Opaque
data:
{{- if eq .Values.backend.auth.mode "simple" }}
JWT_SECRET: {{ randAlphaNum 32 | b64enc | quote }}
{{- end }}
```
### pvc.yaml
```yaml
{{- if .Values.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "cv-app.fullname" . }}-data
labels:
{{- include "cv-app.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
```
### NOTES.txt
```
Your CV application has been deployed!
Access your application at:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}
{{- end }}
{{- else }}
Frontend: http://{{ include "cv-app.fullname" . }}-frontend.{{ .Release.Namespace }}.svc.cluster.local
Backend API: http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001
{{- end }}
{{- if eq .Values.backend.auth.mode "simple" }}
Get the admin password:
kubectl logs deployment/{{ include "cv-app.fullname" . }}-backend | grep "ADMIN PASSWORD"
{{- end }}
API Documentation:
http{{ if .Values.ingress.tls }}s{{ end }}://{{ (index .Values.ingress.hosts 0).host }}/api/docs
```
## Usage Examples
### Basic Installation
```bash
helm install cv-app ./helm/cv-app
```
### With Custom Host
```bash
helm install cv-app ./helm/cv-app \
--set ingress.hosts[0].host=cv.example.com
```
### With TLS
```bash
helm install cv-app ./helm/cv-app \
--set ingress.hosts[0].host=cv.example.com \
--set ingress.tls[0].hosts[0]=cv.example.com \
--set ingress.tls[0].secretName=cv-app-tls
```
### With Keycloak
```bash
helm install cv-app ./helm/cv-app \
--set backend.auth.mode=keycloak \
--set backend.keycloak.url=https://keycloak.example.com \
--set backend.keycloak.realm=myrealm \
--set backend.keycloak.clientId=cv-app
```
### With Specific Storage Class
```bash
helm install cv-app ./helm/cv-app \
--set persistence.storageClass=local-path \
--set persistence.size=5Gi
```

View File

@@ -0,0 +1,636 @@
# Helm Chart Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add Helm chart for Kubernetes deployment with frontend, backend, ingress, and persistence.
**Architecture:** Single Helm chart with templates for Deployments, Services, Ingress, ConfigMap, Secret, and PVC.
**Tech Stack:** Helm 3, Kubernetes
---
### Task 1: Create Helm chart structure
**Files:**
- Create: `helm/cv-app/Chart.yaml`
- Create: `helm/cv-app/.helmignore`
**Step 1: Create helm directory**
```bash
mkdir -p helm/cv-app/templates
```
**Step 2: Create Chart.yaml**
```yaml
apiVersion: v2
name: cv-app
description: A Helm chart for CV application
type: application
version: 0.1.0
appVersion: "1.0.0"
maintainers:
- name: Tuan-Dat Tran
email: tuan-dat.tran@tudattr.dev
```
**Step 3: Create .helmignore**
```
# Patterns to ignore when building packages.
.git/
.github/
.vscode/
.idea/
# Test files
*_test.go
tests/
# Documentation
*.md
!README.md
docs/
# CI/CD
.github/
```
**Step 4: Commit**
```bash
git add helm/cv-app/Chart.yaml helm/cv-app/.helmignore
git commit -m "feat(helm): add chart structure and metadata"
```
---
### Task 2: Create helper templates
**Files:**
- Create: `helm/cv-app/templates/_helpers.tpl`
**Step 1: Create _helpers.tpl**
```yaml
{{- define "cv-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cv-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "cv-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cv-app.labels" -}}
helm.sh/chart: {{ include "cv-app.chart" . }}
{{ include "cv-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "cv-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cv-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "cv-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "cv-app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
```
**Step 2: Commit**
```bash
git add helm/cv-app/templates/_helpers.tpl
git commit -m "feat(helm): add template helpers"
```
---
### Task 3: Create default values
**Files:**
- Create: `helm/cv-app/values.yaml`
**Step 1: Create values.yaml**
```yaml
frontend:
replicaCount: 1
image:
repository: username/cv-app
tag: latest
pullPolicy: IfNotPresent
resources: {}
backend:
replicaCount: 1
image:
repository: username/cv-app-backend
tag: latest
pullPolicy: IfNotPresent
auth:
mode: simple
keycloak:
url: ""
realm: ""
clientId: ""
resources: {}
persistence:
enabled: true
size: 1Gi
storageClass: ""
ingress:
enabled: true
className: ""
annotations: {}
hosts:
- host: cv.local
paths:
- path: /
service: frontend
- path: /api
service: backend
tls: []
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
nodeSelector: {}
tolerations: []
affinity: {}
```
**Step 2: Commit**
```bash
git add helm/cv-app/values.yaml
git commit -m "feat(helm): add default values configuration"
```
---
### Task 4: Create ConfigMap template
**Files:**
- Create: `helm/cv-app/templates/configmap.yaml`
**Step 1: Create configmap.yaml**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "cv-app.fullname" . }}-config
labels:
{{- include "cv-app.labels" . | nindent 4 }}
data:
PORT: "3001"
DB_PATH: "/app/data/cv.db"
AUTH_MODE: {{ .Values.backend.auth.mode | quote }}
{{- if eq .Values.backend.auth.mode "keycloak" }}
KEYCLOAK_URL: {{ .Values.backend.keycloak.url | quote }}
KEYCLOAK_REALM: {{ .Values.backend.keycloak.realm | quote }}
KEYCLOAK_CLIENT_ID: {{ .Values.backend.keycloak.clientId | quote }}
{{- end }}
```
**Step 2: Commit**
```bash
git add helm/cv-app/templates/configmap.yaml
git commit -m "feat(helm): add ConfigMap template"
```
---
### Task 5: Create Secret template
**Files:**
- Create: `helm/cv-app/templates/secret.yaml`
**Step 1: Create secret.yaml**
```yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "cv-app.fullname" . }}-secret
labels:
{{- include "cv-app.labels" . | nindent 4 }}
type: Opaque
data:
{{- if eq .Values.backend.auth.mode "simple" }}
JWT_SECRET: {{ randAlphaNum 32 | b64enc | quote }}
{{- end }}
```
**Step 2: Commit**
```bash
git add helm/cv-app/templates/secret.yaml
git commit -m "feat(helm): add Secret template for JWT"
```
---
### Task 6: Create PVC template
**Files:**
- Create: `helm/cv-app/templates/pvc.yaml`
**Step 1: Create pvc.yaml**
```yaml
{{- if .Values.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "cv-app.fullname" . }}-data
labels:
{{- include "cv-app.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
```
**Step 2: Commit**
```bash
git add helm/cv-app/templates/pvc.yaml
git commit -m "feat(helm): add PersistentVolumeClaim template"
```
---
### Task 7: Create backend Deployment and Service
**Files:**
- Create: `helm/cv-app/templates/backend-deployment.yaml`
- Create: `helm/cv-app/templates/backend-service.yaml`
**Step 1: Create backend-deployment.yaml**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cv-app.fullname" . }}-backend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
replicas: {{ .Values.backend.replicaCount }}
selector:
matchLabels:
{{- include "cv-app.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "cv-app.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: backend
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "cv-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: backend
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
- name: http
containerPort: 3001
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "cv-app.fullname" . }}-config
- secretRef:
name: {{ include "cv-app.fullname" . }}-secret
volumeMounts:
- name: data
mountPath: /app/data
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
resources:
{{- toYaml .Values.backend.resources | nindent 12 }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "cv-app.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
```
**Step 2: Create backend-service.yaml**
```yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "cv-app.fullname" . }}-backend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
type: ClusterIP
ports:
- port: 3001
targetPort: http
protocol: TCP
name: http
selector:
{{- include "cv-app.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: backend
```
**Step 3: Commit**
```bash
git add helm/cv-app/templates/backend-deployment.yaml helm/cv-app/templates/backend-service.yaml
git commit -m "feat(helm): add backend Deployment and Service templates"
```
---
### Task 8: Create frontend Deployment and Service
**Files:**
- Create: `helm/cv-app/templates/frontend-deployment.yaml`
- Create: `helm/cv-app/templates/frontend-service.yaml`
**Step 1: Create frontend-deployment.yaml**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cv-app.fullname" . }}-frontend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
{{- include "cv-app.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: frontend
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "cv-app.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: frontend
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "cv-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: frontend
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.frontend.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
```
**Step 2: Create frontend-service.yaml**
```yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "cv-app.fullname" . }}-frontend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
{{- include "cv-app.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: frontend
```
**Step 3: Commit**
```bash
git add helm/cv-app/templates/frontend-deployment.yaml helm/cv-app/templates/frontend-service.yaml
git commit -m "feat(helm): add frontend Deployment and Service templates"
```
---
### Task 9: Create Ingress template
**Files:**
- Create: `helm/cv-app/templates/ingress.yaml`
**Step 1: Create ingress.yaml**
```yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "cv-app.fullname" . }}
labels:
{{- include "cv-app.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: Prefix
backend:
service:
name: {{ include "cv-app.fullname" $ }}-{{ .service }}
port:
number: {{ if eq .service "frontend" }}80{{ else }}3001{{ end }}
{{- end }}
{{- end }}
{{- end }}
```
**Step 2: Commit**
```bash
git add helm/cv-app/templates/ingress.yaml
git commit -m "feat(helm): add Ingress template"
```
---
### Task 10: Create NOTES.txt and update documentation
**Files:**
- Create: `helm/cv-app/templates/NOTES.txt`
- Modify: `docs/architecture.md`
**Step 1: Create NOTES.txt**
```
Your CV application has been deployed!
Access your application at:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}
{{- end }}
{{- else }}
Frontend: http://{{ include "cv-app.fullname" . }}-frontend.{{ .Release.Namespace }}.svc.cluster.local
Backend API: http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001
{{- end }}
{{- if eq .Values.backend.auth.mode "simple" }}
Get the admin password:
kubectl logs deployment/{{ include "cv-app.fullname" . }}-backend | grep "ADMIN PASSWORD"
{{- end }}
API Documentation:
{{- if .Values.ingress.enabled }}
{{- if .Values.ingress.tls }}
https://{{ (index .Values.ingress.hosts 0).host }}/api/docs
{{- else }}
http://{{ (index .Values.ingress.hosts 0).host }}/api/docs
{{- end }}
{{- else }}
http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001/api/docs
{{- end }}
```
**Step 2: Commit**
```bash
git add helm/cv-app/templates/NOTES.txt
git commit -m "feat(helm): add post-install notes"
```

View File

@@ -4,7 +4,7 @@
## Overview
This project uses a fully automated release engineering pipeline powered by **semantic-release**, **commitlint**, and **GitHub Actions**. The pipeline handles versioning, changelog generation, Docker image publishing, and multi-environment deployments.
This project uses a fully automated release engineering pipeline powered by **semantic-release**, **commitlint**, and **Gitea Actions**. The pipeline handles versioning, changelog generation, and releases. Docker publishing is currently disabled pending runner configuration.
## Architecture
@@ -22,36 +22,20 @@ This project uses a fully automated release engineering pipeline powered by **se
┌─────────────────────────────────────────────────────────────────────────────┐
│ GitHub Actions Pipeline │
Gitea Actions Pipeline │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ CI Job │───►│ Release Job │───►│ Docker Build Job │ │
│ │ │ │ │ │
│ │ • Lint │ │ • Analyze │ │ • Multi-platform build │ │
│ │ • Test │ │ commits (amd64, arm64) │ │
│ │ • Build │ │ • Bump version │ │ • Push to Docker Hub │ │
│ │ • Commitlint│ │ • Update │ • Push to GHCR │ │
│ │ │ │ CHANGELOG │ │ │ │
│ │ │ • Create tag │ │ │
│ │ │ │ • GitHub release│ │ │ │
│ └─────────────┘ └─────────────────┘ └──────────────────────────┘ │
│ ┌─────────────┐ ┌─────────────────────────────────────────────────┐ │
│ │ CI Job │───►│ Release Job │ │
│ │ │ │ │ │
│ │ • Lint │ │ • Analyze commits │ │
│ │ • Test │ │ • Bump version │ │
│ │ • Build │ │ • Update CHANGELOG │ │
│ │ • Commitlint│ │ • Create tag │ │
│ │ │ │ Create Gitea release │ │
└─────────────┘ └─────────────────────────────────────────────────┘
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Registries │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ Docker Hub │ │ GitHub Container Registry │ │
│ │ │ │ │ │
│ │ username/cv-app:latest │ │ ghcr.io/owner/cv-app:latest │ │
│ │ username/cv-app:v1.0.0 │ │ ghcr.io/owner/cv-app:v1.0.0 │ │
│ │ username/cv-app:staging │ │ ghcr.io/owner/cv-app:staging │ │
│ │ username/cv-app:nightly │ │ ghcr.io/owner/cv-app:nightly │ │
│ └─────────────────────────────┘ └─────────────────────────────────┘ │
│ Note: Docker publishing temporarily disabled (runner lacks Docker) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -128,11 +112,20 @@ Located in `.releaserc.json`:
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git",
"@semantic-release/github"
[
"@semantic-release/github",
{
"successCommentCondition": false,
"failCommentCondition": false,
"releasedLabels": false
}
]
]
}
```
Note: The `successCommentCondition` and `failCommentCondition` are set to `false` for Gitea compatibility (Gitea lacks GitHub's GraphQL API).
### 3. commitlint
Enforces conventional commits via:
@@ -149,12 +142,14 @@ Configuration in `commitlint.config.js`:
### 4. Husky Git Hooks
| Hook | Purpose | Command |
| ------------ | ------------------------- | ------------------- |
| `pre-commit` | Run linting before commit | `npm run lint` |
| `commit-msg` | Validate commit message | `commitlint --edit` |
| Hook | Purpose | Command |
| ------------ | ------------------------- | ------------------------------------ |
| `pre-commit` | Run linting + secret scan | `npm run lint` + `gitleaks protect` |
| `commit-msg` | Validate commit message | `commitlint --edit` |
### 5. GitHub Actions Workflows
Note: Gitleaks scans for secrets before commit. If not installed locally, it's skipped gracefully.
### 5. Gitea Actions Workflows
#### CI Workflow (`.github/workflows/ci.yml`)
@@ -193,8 +188,9 @@ Runs on: Push to master (excluding [skip ci] commits)
│ Release Job │
├───────────────────────────────────────────────────────────┤
│ 1. Checkout (fetch-depth: 0 for full history) │
│ 2. Setup Node.js 20
│ 2. Setup Node.js 24
│ 3. Install dependencies (root + backend) │
│ - Uses --prefer-offline --no-audit --no-fund │
│ 4. Lint │
│ 5. Run tests (root + backend) │
│ 6. Build │
@@ -203,75 +199,41 @@ Runs on: Push to master (excluding [skip ci] commits)
│ - Bump version │
│ - Update CHANGELOG.md │
│ - Create git tag │
│ - Create GitHub release │
└───────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ Docker Build Job │
├───────────────────────────────────────────────────────────┤
│ 1. Checkout │
│ 2. Get version from git tag │
│ 3. Setup QEMU (for multi-platform) │
│ 4. Setup Docker Buildx │
│ 5. Login to Docker Hub │
│ 6. Login to GHCR │
│ 7. Build and push (amd64 + arm64) │
│ - Create Gitea release
└───────────────────────────────────────────────────────────┘
```
Note: Docker build job temporarily disabled (runner lacks Docker support).
#### Staging Workflow (`.github/workflows/staging.yml`)
Runs on: Push to staging branch
```
┌───────────────────────────────────────────────────────────┐
│ Staging Deploy Job │
├───────────────────────────────────────────────────────────┤
│ 1. Checkout │
│ 2. Install + Lint + Test + Build │
│ 3. Build Docker image (multi-platform) │
│ 4. Push with tag: staging │
└───────────────────────────────────────────────────────────┘
```
**Status: Temporarily disabled** - Requires Docker support on runner.
#### Nightly Workflow (`.github/workflows/nightly.yml`)
Runs on: Schedule (daily at 02:00 UTC)
```
┌───────────────────────────────────────────────────────────┐
│ Nightly Build Job │
├───────────────────────────────────────────────────────────┤
│ 1. Checkout master │
│ 2. Get current date │
│ 3. Install + Build │
│ 4. Build Docker image (multi-platform) │
│ 5. Push with tags: nightly, edge, YYYY-MM-DD │
└───────────────────────────────────────────────────────────┘
```
**Status: Temporarily disabled** - Requires Docker support on runner.
## Docker Image Tags
**Status: Temporarily disabled** - Docker publishing requires runner with Docker-in-Docker support.
| Tag | Registry | Description | Update Frequency |
| ------------ | -------- | --------------------- | ---------------------- |
| `latest` | Both | Latest stable release | Every release |
| `v1.0.0` | Both | Specific version | Immutable |
| `1.0` | Both | Major.minor | Points to latest patch |
| `1` | Both | Major version | Points to latest minor |
| `staging` | Both | Staging environment | Every staging push |
| `nightly` | Both | Latest nightly build | Daily |
| `edge` | Both | Alias for nightly | Daily |
| `2026-02-20` | Both | Date-specific nightly | Immutable |
### Pulling Images
### Pulling Images (when Docker publishing is enabled)
```bash
# Docker Hub
docker pull username/cv-app:latest
docker pull username/cv-app:v1.0.0
docker pull username/cv-app:staging
docker pull username/cv-app:nightly
# GitHub Container Registry
docker pull ghcr.io/owner/cv-app:latest
@@ -280,11 +242,11 @@ docker pull ghcr.io/owner/cv-app:v1.0.0
## Environments
| Environment | Branch | Trigger | Docker Tag |
| ----------- | --------- | ----------------------------------- | ------------------------------- |
| Production | `master` | semantic-release (feat/fix commits) | `latest`, `vX.Y.Z` |
| Staging | `staging` | Push to branch | `staging` |
| Nightly | `master` | Daily at 02:00 UTC | `nightly`, `edge`, `YYYY-MM-DD` |
| Environment | Branch | Trigger | Status |
| ----------- | --------- | ----------------------------------- | ---------------------------- |
| Production | `master` | semantic-release (feat/fix commits) | Active (release only) |
| Staging | `staging` | Push to branch | Disabled (Docker required) |
| Nightly | `master` | Daily at 02:00 UTC | Disabled (Docker required) |
## Release Flow Example
@@ -296,6 +258,7 @@ Developer commits: feat(admin): add export functionality
│ Git Hook Runs │
│ (pre-commit) │
│ • npm run lint │
│ • gitleaks │
└─────────────────┘
@@ -361,40 +324,23 @@ Developer commits: feat(admin): add export functionality
┌─────────────────┐
│ GitHub Release │
│ Gitea Release
│ created │
│ with notes │
└─────────────────┘
┌─────────────────────────────────┐
│ Docker images built and │
│ pushed to: │
│ │
│ • username/cv-app:latest │
│ • username/cv-app:v1.1.0 │
│ • ghcr.io/owner/cv-app:latest │
│ • ghcr.io/owner/cv-app:v1.1.0 │
└─────────────────────────────────┘
```
## Required Secrets
Configure these in GitHub repository settings → Secrets and variables → Actions:
Configure these in Gitea repository settings → Settings → Secrets:
| Secret | Description | Required For |
| -------------------- | ------------------------ | ------------------- |
| `DOCKERHUB_USERNAME` | Docker Hub username | Docker Hub push |
| `DOCKERHUB_TOKEN` | Docker Hub access token | Docker Hub push |
| `GITHUB_TOKEN` | GitHub token (automatic) | GHCR push, releases |
| `GITHUB_TOKEN` | Gitea token (automatic) | Releases |
### Creating Docker Hub Token
1. Go to Docker Hub → Account Settings → Security
2. Click "New Access Token"
3. Name: `cv-app-github-actions`
4. Permissions: Read, Write, Delete
5. Copy token and add to GitHub secrets
Note: Docker publishing is currently disabled until runner supports Docker-in-Docker.
## Local Development
@@ -449,16 +395,6 @@ git commit -m "feat(ui): add new button"
**Solution**: Ensure you have `feat`, `fix`, or `perf` commits since last release.
### Docker Push Fails
**Error**: `denied: requested access to the resource is denied`
**Fix**:
1. Verify `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets
2. Ensure Docker Hub token has Write permission
3. Check Docker Hub repository exists (or enable auto-create)
### Version Not Bumping
**Cause**: semantic-release requires conventional commits with proper types.

17
helm/cv-app/.helmignore Normal file
View File

@@ -0,0 +1,17 @@
# Patterns to ignore when building packages.
.git/
.github/
.vscode/
.idea/
# Test files
*_test.go
tests/
# Documentation
*.md
!README.md
docs/
# CI/CD
.github/

9
helm/cv-app/Chart.yaml Normal file
View File

@@ -0,0 +1,9 @@
apiVersion: v2
name: cv-app
description: A Helm chart for CV application
type: application
version: 0.1.0
appVersion: "1.0.0"
maintainers:
- name: Tuan-Dat Tran
email: tuan-dat.tran@tudattr.dev

View File

@@ -0,0 +1,27 @@
Your CV application has been deployed!
Access your application at:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}
{{- end }}
{{- else }}
Frontend: http://{{ include "cv-app.fullname" . }}-frontend.{{ .Release.Namespace }}.svc.cluster.local
Backend API: http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001
{{- end }}
{{- if eq .Values.backend.auth.mode "simple" }}
Get the admin password:
kubectl logs deployment/{{ include "cv-app.fullname" . }}-backend | grep "ADMIN PASSWORD"
{{- end }}
API Documentation:
{{- if .Values.ingress.enabled }}
{{- if .Values.ingress.tls }}
https://{{ (index .Values.ingress.hosts 0).host }}/api/docs
{{- else }}
http://{{ (index .Values.ingress.hosts 0).host }}/api/docs
{{- end }}
{{- else }}
http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001/api/docs
{{- end }}

View File

@@ -0,0 +1,42 @@
{{- define "cv-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cv-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "cv-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "cv-app.labels" -}}
helm.sh/chart: {{ include "cv-app.chart" . }}
{{ include "cv-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "cv-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cv-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "cv-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "cv-app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cv-app.fullname" . }}-backend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
replicas: {{ .Values.backend.replicaCount }}
selector:
matchLabels:
{{- include "cv-app.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "cv-app.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: backend
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "cv-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: backend
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
- name: http
containerPort: 3001
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "cv-app.fullname" . }}-config
- secretRef:
name: {{ include "cv-app.fullname" . }}-secret
volumeMounts:
- name: data
mountPath: /app/data
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
resources:
{{- toYaml .Values.backend.resources | nindent 12 }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "cv-app.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "cv-app.fullname" . }}-backend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
type: ClusterIP
ports:
- port: 3001
targetPort: http
protocol: TCP
name: http
selector:
{{- include "cv-app.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: backend

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "cv-app.fullname" . }}-config
labels:
{{- include "cv-app.labels" . | nindent 4 }}
data:
PORT: "3001"
DB_PATH: "/app/data/cv.db"
AUTH_MODE: {{ .Values.backend.auth.mode | quote }}
{{- if eq .Values.backend.auth.mode "keycloak" }}
KEYCLOAK_URL: {{ .Values.backend.keycloak.url | quote }}
KEYCLOAK_REALM: {{ .Values.backend.keycloak.realm | quote }}
KEYCLOAK_CLIENT_ID: {{ .Values.backend.keycloak.clientId | quote }}
{{- end }}

View File

@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cv-app.fullname" . }}-frontend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
{{- include "cv-app.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: frontend
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "cv-app.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: frontend
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "cv-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: frontend
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.frontend.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "cv-app.fullname" . }}-frontend
labels:
{{- include "cv-app.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
{{- include "cv-app.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: frontend

View File

@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "cv-app.fullname" . }}
labels:
{{- include "cv-app.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: Prefix
backend:
service:
name: {{ include "cv-app.fullname" $ }}-{{ .service }}
port:
number: {{ if eq .service "frontend" }}80{{ else }}3001{{ end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,17 @@
{{- if .Values.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "cv-app.fullname" . }}-data
labels:
{{- include "cv-app.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}

View File

@@ -0,0 +1,11 @@
{{- if eq .Values.backend.auth.mode "simple" }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "cv-app.fullname" . }}-secret
labels:
{{- include "cv-app.labels" . | nindent 4 }}
type: Opaque
data:
JWT_SECRET: {{ .Values.backend.auth.jwtSecret | default (randAlphaNum 32) | b64enc | quote }}
{{- end }}

57
helm/cv-app/values.yaml Normal file
View File

@@ -0,0 +1,57 @@
frontend:
replicaCount: 1
image:
repository: username/cv-app
tag: latest
pullPolicy: IfNotPresent
resources: {}
backend:
replicaCount: 1
image:
repository: username/cv-app-backend
tag: latest
pullPolicy: IfNotPresent
auth:
mode: simple
jwtSecret: ""
keycloak:
url: ""
realm: ""
clientId: ""
resources: {}
persistence:
enabled: true
size: 1Gi
storageClass: ""
ingress:
enabled: true
className: ""
annotations: {}
hosts:
- host: cv.local
paths:
- path: /api
service: backend
- path: /
service: frontend
tls: []
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -1,7 +1,7 @@
{
"name": "cv-app",
"private": true,
"version": "1.0.0",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -51,7 +51,7 @@
"@testing-library/react": "^16.3.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^6.0.0",
"@vitest/coverage-v8": "^4.0.18",
"autoprefixer": "^10.4.24",
"better-sqlite3": "^12.6.2",

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}