Skip to main content
CharleOS uses GitHub Actions for continuous integration and Vercel for continuous deployment. This page explains the automated pipeline.

CI/CD Overview

Developer pushes code

GitHub Actions (CI)
  ├── Lint code
  ├── Type check
  ├── Run unit tests
  ├── Run E2E tests
  └── Build application

All checks pass?
  ├── Yes → Vercel deploys (CD)
  └── No → Block merge

GitHub Actions Workflows

CharleOS has several automated workflows:

CI Pipeline

Runs on every PR to main
  • Linting
  • Type checking
  • Unit tests
  • Build verification

E2E Tests

Runs on PRs (separate workflow)
  • Playwright E2E tests
  • Critical user flows
  • Browser compatibility

Dependabot

Automatic dependency updates
  • Security patches
  • Minor version bumps
  • Auto-merge for tests

Release

Automated changelog and versioning
  • Conventional commits
  • Semantic versioning
  • GitHub releases

CI Workflow (.github/workflows/ci.yml)

Workflow Configuration

name: CI

on:
  pull_request:
    branches: [main]

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      
      - name: Install dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm ci

Lint Job

lint:
  needs: setup
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    
    - name: Restore dependencies
      uses: actions/cache/restore@v4
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    
    - name: Run ESLint
      run: npm run lint
What it checks:
  • Code style (Prettier)
  • ESLint rules
  • Import order
  • Unused variables

Type Check Job

type-check:
  needs: setup
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    
    - name: Restore dependencies
      uses: actions/cache/restore@v4
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    
    - name: Run TypeScript compiler
      run: npm run type-check
What it checks:
  • TypeScript compilation errors
  • Type mismatches
  • Missing type definitions

Unit Tests Job

unit-tests:
  needs: setup
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    
    - name: Restore dependencies
      uses: actions/cache/restore@v4
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    
    - name: Run unit tests with coverage
      run: npm run test:coverage
    
    - name: Upload coverage report
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: coverage-report
        path: coverage/
What it tests:
  • Business logic
  • Utility functions
  • Billing calculations
  • Date/time utilities

Build Job

build:
  needs: setup
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    
    - name: Restore dependencies
      uses: actions/cache/restore@v4
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    
    - name: Cache Next.js build
      uses: actions/cache@v4
      with:
        path: .next/cache
        key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}
    
    - name: Build application
      run: npm run build
      env:
        # Dummy env vars for build
        DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy"
        BETTER_AUTH_SECRET: "dummy-secret-for-build-must-be-32-chars-long"
        BETTER_AUTH_URL: "http://localhost:3000"
        GOOGLE_CLIENT_ID: "dummy"
        GOOGLE_CLIENT_SECRET: "dummy"
What it checks:
  • Next.js builds successfully
  • No build errors
  • TypeScript compiles in build mode

Security Audit

security-audit:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with:
        sparse-checkout: |
          package.json
          package-lock.json
    
    - name: Run npm audit
      run: npm audit --audit-level=high
      continue-on-error: true
What it checks:
  • Known vulnerabilities in dependencies
  • Security advisories
  • High/critical severity issues

E2E Workflow (.github/workflows/e2e.yml)

name: E2E Tests

on:
  pull_request:
    branches: [main]

jobs:
  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright
        run: npx playwright install --with-deps
      
      - name: Run E2E tests
        run: npm run test:e2e
        env:
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
What it tests:
  • Authentication flows
  • Critical user journeys
  • Task/quote workflows
  • Client portal functionality

Dependabot Configuration

Automatic dependency updates:
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "lukespoor"
    assignees:
      - "lukespoor"
Auto-merge workflow:
# .github/workflows/dependabot-auto-merge.yml
name: Dependabot Auto-Merge

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  auto-merge:
    if: github.actor == 'dependabot[bot]'
    runs-on: ubuntu-latest
    steps:
      - name: Wait for CI to pass
        uses: lewagon/[email protected]
        with:
          check-name: 'CI'
          
      - name: Auto-merge patch/minor updates
        if: |
          startsWith(github.event.pull_request.title, 'Bump') &&
          (contains(github.event.pull_request.title, 'patch') ||
           contains(github.event.pull_request.title, 'minor'))
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Branch Protection Rules

main branch is protected with: Required checks:
  • ✅ Lint
  • ✅ Type check
  • ✅ Unit tests
  • ✅ Build
  • ✅ E2E tests
Additional rules:
  • Require PR review (1 approval)
  • Dismiss stale reviews on new push
  • Require linear history
  • No force push allowed

Continuous Deployment

Vercel Integration

Vercel automatically deploys: Production:
  • Trigger: Push to main
  • URL: https://charle.agency
  • Only after CI passes
Preview:
  • Trigger: Open/update PR
  • URL: https://charle-os-git-{branch}.vercel.app
  • Deploys even if CI fails (for testing)

Deployment Process

PR merged to main

GitHub triggers Vercel webhook

Vercel Build
  ├── Install dependencies
  ├── Run migrations (postinstall)
  ├── Build Next.js
  └── Upload to CDN

Live at charle.agency

Release Workflow

Automated releases using Release Please:
# .github/workflows/release.yml
name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          release-type: node
          package-name: charle-os
How it works:
  1. Analyzes conventional commits since last release
  2. Generates changelog
  3. Bumps version in package.json
  4. Creates GitHub release
  5. Tags commit
Commit conventions:
feat: Add new feature          → Minor version bump
fix: Fix bug                   → Patch version bump
feat!: Breaking change         → Major version bump
chore: Update dependencies     → No release

Monitoring CI/CD

GitHub Actions Dashboard

View workflow runs: URL: https://github.com/charle/charle-os/actions Information:
  • All workflow runs
  • Duration and status
  • Logs for each step
  • Artifacts (coverage reports, test results)

Notifications

Failed CI sends notifications via:
  • GitHub UI
  • Email (to PR author)
  • Slack (optional integration)

Performance Optimization

Caching

Cache speeds up CI significantly: Dependencies:
- uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
Next.js build:
- uses: actions/cache@v4
  with:
    path: .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/*.ts', '**/*.tsx') }}
Playwright browsers:
- uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: ${{ runner.os }}-playwright

Parallelization

Jobs run in parallel when possible:
Setup (1 min)

┌───────────┬──────────────┬─────────────┬────────┐
│ Lint      │ Type Check   │ Unit Tests  │ Build  │
│ (30s)     │ (45s)        │ (1 min)     │ (2 min)│
└───────────┴──────────────┴─────────────┴────────┘

All pass → Merge allowed
Total time: ~3 minutes (vs. 5+ minutes sequentially)

Troubleshooting

CI Failing

Error: “Lint failed” Fix:
# Run locally
npm run lint

# Auto-fix issues
npm run lint -- --fix

# Commit fixes
git add .
git commit -m "fix: lint errors"
Error: “Type check failed” Fix:
# Run locally
npm run type-check

# Fix TypeScript errors
# Then commit
Error: “Tests failed” Fix:
# Run locally
npm run test

# Debug specific test
npm run test -- -t "failing test name"

# Fix and commit

Dependabot PRs Not Auto-Merging

Cause: CI not passing or version is major bump Fix:
  1. Check CI logs
  2. Fix any test failures
  3. For major bumps, review manually

E2E Tests Flaky

Cause: Timing issues, race conditions Fix:
  1. Add explicit waits: await page.waitForSelector(...)
  2. Use waitForLoadState('networkidle')
  3. Increase timeouts for slow operations

Best Practices

Before pushing:
npm run lint
npm run type-check
npm run test
npm run build
Use conventional commits:
feat: add new feature
fix: resolve bug
chore: update dependencies
docs: update readme
test: add missing tests
Don’t ignore failed CI:
  • Fix failures immediately
  • Don’t merge with failing tests
  • Investigate flaky tests
Optimize for speed:
  • Use caching
  • Parallelize jobs
  • Only run necessary checks