13 Commits

Author SHA1 Message Date
Renovate Bot
6c9d4740e6 chore(deps): update dependency eslint-plugin-react-refresh to ^0.5.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-03-04 00:01:24 +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
34 changed files with 2419 additions and 3201 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

@@ -61,6 +61,13 @@
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
[
"@semantic-release/github",
{
"successCommentCondition": false,
"failCommentCondition": false,
"releasedLabels": false
}
]
]
}

View File

@@ -2,6 +2,36 @@
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

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.1",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -59,7 +59,7 @@
"cors": "^2.8.6",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-refresh": "^0.5.0",
"express": "^5.2.1",
"globals": "^16.5.0",
"husky": "^9.1.7",

3
renovate.json Normal file
View File

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