Files
kilo-cv/docs/plans/2026-02-20-release-engineering.md
2026-02-20 09:57:00 +01:00

22 KiB

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

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

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:

{
  "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

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:

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

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

npx husky init

Expected: .husky/ directory created

Step 2: Create commit-msg hook

Replace .husky/commit-msg content with:

npx --no -- commitlint --edit "$1"

Step 3: Create pre-commit hook

Replace .husky/pre-commit content with:

npm run lint

Step 4: Make hooks executable

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:

"prepare": "husky"

Step 6: Commit

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:

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

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:

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

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:

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

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:

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

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:

  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:

  integration:
    runs-on: ubuntu-latest
    needs: [frontend, backend]

to:

  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

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:

# 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

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:

## 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:

():

[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

git add package.json
git commit -m "chore: set initial development version"

Task 13: Verify All Configurations

Step 1: Run lint

npm run lint

Expected: No errors

Step 2: Run tests

npm run test:run

Expected: All tests pass

Step 3: Test commitlint locally

echo "feat(test): test commit message" | npx commitlint

Expected: No errors

echo "invalid commit message" | npx commitlint

Expected: Error message about invalid format

Step 4: Verify husky hooks

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

git status

Expected: "nothing to commit, working tree clean" or only untracked files that shouldn't be committed

Step 2: View commit history

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