Compare commits
8 Commits
v1.0.3
...
fix/replac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ab7ba702 | ||
|
|
c18e4dda10 | ||
| 8deb3dcd9f | |||
|
|
8b5e12fa0a | ||
|
|
d73e8769c0 | ||
|
|
8d7000eb31 | ||
|
|
dd3b83f7e1 | ||
|
|
bd44ea42d7 |
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 -->
|
||||
|
||||
32
.github/dependabot.yml
vendored
32
.github/dependabot.yml
vendored
@@ -1,32 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- "tuan-dat-tran"
|
||||
labels:
|
||||
- "dependencies"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
include: "scope"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 3
|
||||
reviewers:
|
||||
- "tuan-dat-tran"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
1
.github/workflows/nightly.yml
vendored
1
.github/workflows/nightly.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
name: Build Nightly Image
|
||||
runs-on: ubuntu-latest
|
||||
environment: nightly
|
||||
if: false
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/staging.yml
vendored
1
.github/workflows/staging.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
name: Build & Deploy to Staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
if: false
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,6 +23,9 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Worktrees
|
||||
.worktrees/
|
||||
|
||||
# Database
|
||||
backend/data/
|
||||
*.db
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -2,6 +2,24 @@
|
||||
|
||||
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
|
||||
|
||||
180
CONTRIBUTING.md
180
CONTRIBUTING.md
@@ -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
194
README.md
@@ -1,8 +1,8 @@
|
||||
# CV - Tuan-Dat Tran
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
131
SECURITY.md
131
SECURITY.md
@@ -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
448
docs/architecture.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/)
|
||||
@@ -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
|
||||
491
docs/plans/2026-02-23-helm-chart-design.md
Normal file
491
docs/plans/2026-02-23-helm-chart-design.md
Normal 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
|
||||
```
|
||||
636
docs/plans/2026-02-23-helm-chart-implementation.md
Normal file
636
docs/plans/2026-02-23-helm-chart-implementation.md
Normal 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"
|
||||
```
|
||||
@@ -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
17
helm/cv-app/.helmignore
Normal 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
9
helm/cv-app/Chart.yaml
Normal 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
|
||||
27
helm/cv-app/templates/NOTES.txt
Normal file
27
helm/cv-app/templates/NOTES.txt
Normal 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 }}
|
||||
42
helm/cv-app/templates/_helpers.tpl
Normal file
42
helm/cv-app/templates/_helpers.tpl
Normal 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 }}
|
||||
80
helm/cv-app/templates/backend-deployment.yaml
Normal file
80
helm/cv-app/templates/backend-deployment.yaml
Normal 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 }}
|
||||
17
helm/cv-app/templates/backend-service.yaml
Normal file
17
helm/cv-app/templates/backend-service.yaml
Normal 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
|
||||
15
helm/cv-app/templates/configmap.yaml
Normal file
15
helm/cv-app/templates/configmap.yaml
Normal 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 }}
|
||||
63
helm/cv-app/templates/frontend-deployment.yaml
Normal file
63
helm/cv-app/templates/frontend-deployment.yaml
Normal 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 }}
|
||||
17
helm/cv-app/templates/frontend-service.yaml
Normal file
17
helm/cv-app/templates/frontend-service.yaml
Normal 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
|
||||
41
helm/cv-app/templates/ingress.yaml
Normal file
41
helm/cv-app/templates/ingress.yaml
Normal 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 }}
|
||||
17
helm/cv-app/templates/pvc.yaml
Normal file
17
helm/cv-app/templates/pvc.yaml
Normal 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 }}
|
||||
11
helm/cv-app/templates/secret.yaml
Normal file
11
helm/cv-app/templates/secret.yaml
Normal 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
57
helm/cv-app/values.yaml
Normal 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: {}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cv-app",
|
||||
"private": true,
|
||||
"version": "1.0.3",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
37
renovate.json
Normal file
37
renovate.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"timezone": "Europe/Berlin",
|
||||
"schedule": ["on monday before 10:00am"],
|
||||
"prConcurrentLimit": 5,
|
||||
"prHourlyLimit": 2,
|
||||
"labels": ["dependencies"],
|
||||
"commitMessagePrefix": "deps",
|
||||
"commitMessageAction": "update",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"rangeStrategy": "bump",
|
||||
"semanticCommitType": "deps",
|
||||
"semanticCommitScope": "{{packageFileDir}}"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"semanticCommitType": "ci",
|
||||
"semanticCommitScope": "actions",
|
||||
"groupName": "github-actions"
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["on the first day of the month"]
|
||||
},
|
||||
"postUpdateOptions": ["npmDedupe"]
|
||||
}
|
||||
Reference in New Issue
Block a user