docs: add release engineering implementation plan
This commit is contained in:
974
docs/plans/2026-02-20-release-engineering.md
Normal file
974
docs/plans/2026-02-20-release-engineering.md
Normal file
@@ -0,0 +1,974 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user