CI/CD Pipeline
This boilerplate comes with a fully configured GitHub Actions pipeline to ensure code quality and automate deployments. The pipeline is designed to be zero-configuration for public repositories and provides a complete DevOps workflow out of the box.
Overview
The CI/CD system follows a Release Train model using a staging branch for stabilization before production:
Workflows
1. Quality Gate (ci.yml)
Purpose: Enforce code quality standards before merging
Triggers:
- Every
pushtomainorstagingbranches - Every
pull_requesttargetingmainorstaging
Optimization:
- Concurrency: Includes a
concurrencygroup to cancel redundant runs. - Caching: Uses
actions/cacheto cachebundependencies (~/.bun/install/cache), significantly reducing install times.
Jobs:
Lint
- name: Lint Check
run: bun run lint:checkChecks code style using Biome. Ensures consistent formatting and catches common errors.
Test
- name: Run Tests
run: bun run testsRuns the full test suite with coverage reporting.
Build
- name: Build Project
run: bun run buildVerifies the project builds successfully for production.
Configuration:
# .github/workflows/ci.yml
name: Quality Gate
on:
push:
branches: [main, staging]
pull_request:
branches: [main, staging]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- run: bun install
- run: bun run lint:check
# ... (similar for test and build)2. PR Labeler (labeler.yml)
Purpose: Automatically categorize Pull Requests based on the files they modify.
What it does:
- Assigns labels like
feat,fix,docs,ci, etc., automatically. - Helps maintainers quickly understand the scope of a PR.
- Syncs labels if file changes are added to the PR.
3. Release Train (release-train.yml)
Purpose: Automate the stabilization process between staging and main branches.
Triggers:
- Scheduled (Weekly on Mondays)
- Manual trigger (Workflow Dispatch)
What it does:
- Checks for differences between
stagingandmain. - Generates a release body summarizing commits.
- Validates permissions and uses
gh pr createto safely open a PR without overwriting branch history.
4. Automated Release (release.yml)
Purpose: Automatically version, tag, and release based on commit messages
Triggers:
- After Quality Gate passes on
main - Uses
workflow_runto ensure sequential execution
Tool: Semantic Release
How it works:
Commit Message Format:
Uses Conventional Commits:
| Type | Version Bump | Example |
|---|---|---|
feat: | Minor (1.0.0 → 1.1.0) | feat: add user authentication |
fix: | Patch (1.0.0 → 1.0.1) | fix: resolve login bug |
BREAKING CHANGE: | Major (1.0.0 → 2.0.0) | feat!: redesign API |
docs:, chore: | No release | docs: update README |
Configuration:
// package.json
{
"release": {
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/npm", { "npmPublish": false }],
"@semantic-release/github"
]
}
}Note:
npmPublish: falsedisables npm publishing. This boilerplate is not published to npm by default. Remove this option if you want to publish packages.
3. Docker Deployment (deploy-docker.yml)
Purpose: Build and push Docker images to GitHub Container Registry (GHCR)
Triggers:
- When a new GitHub Release is published
What it does:
- Builds Docker image from
Dockerfile - Tags image with release version
- Pushes to
ghcr.io/<username>/<repo>
Configuration:
# .github/workflows/deploy-docker.yml
name: Docker Deployment
on:
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}Pulling the image:
docker pull ghcr.io/<username>/<repo>:latestLocal Quality Checks
Before pushing, it is recommended to run local checks. This project uses Client-side Git Hooks managed by Lefthook.
Hooks Enforced (lefthook.yml):
- Pre-commit: Runs
biome check --writeon staged files. Applies formatting fixes automatically. - Pre-push: Runs
bun testto ensure tests pass before pushing to remote.
Manual Commands:
# Check code style manually
bun run lint:check
# Run tests manually
bun run tests
# Verify build
bun run buildTroubleshooting
Quality Gate Failing
Lint Errors
Error: Found X errors
Solution:
# Auto-fix most issues
bunx biome check --write .
# Check what's wrong
bun run lint:checkTest Failures
Error: X tests failed
Solution:
# Run tests locally
bun test
# Run specific test
bun test tests/unit/domain/user/user-get-by-id.spec.ts
# Run with watch mode
bun test --watchBuild Errors
Error: Build failed
Solution:
# Check TypeScript errors
bun run build
# Clear cache and rebuild
rm -rf dist && bun run buildRelease Not Triggering
Problem: Pushed to main but no release created
Possible causes:
Commit message doesn't follow convention
bash# ❌ Wrong git commit -m "added new feature" # ✅ Correct git commit -m "feat: add user authentication"Quality Gate failed
- Check Actions tab for errors
- Fix issues and push again
No releasable commits
docs:andchore:don't trigger releases- Need at least one
feat:orfix:commit
Docker Push Failing
Error: denied: permission_denied
Solution:
- Enable GitHub Packages in repository settings
- Ensure
GITHUB_TOKENhaspackages: writepermission - Make repository public or configure package visibility
Advanced Configuration
Custom Release Rules
Modify package.json to customize release behavior:
{
"release": {
"branches": ["main", "next"],
"plugins": [
["@semantic-release/commit-analyzer", {
"releaseRules": [
{"type": "docs", "release": "patch"},
{"type": "refactor", "release": "patch"},
{"scope": "no-release", "release": false}
]
}]
]
}
}Deploy to Multiple Registries
Push to both GHCR and Docker Hub:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/myapp:latestAdd Deployment Environments
Deploy to staging/production after release:
# .github/workflows/deploy-production.yml
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
run: |
# Your deployment script
ssh user@server 'docker pull ghcr.io/...'Best Practices
- Write good commit messages: Follow Conventional Commits strictly
- Run checks locally: Don't rely solely on CI
- Keep workflows fast: Parallel jobs when possible
- Monitor Actions usage: GitHub has monthly limits
- Secure secrets: Never commit credentials
- Test in branches: Use feature branches and PRs
See Also:
- Deployment - Production deployment guide
- Testing Guide - Writing and running tests
- Troubleshooting - Common issues and solutions