Aller au contenu principal

CI/CD Pipeline with GitHub Actions

This example shows how to create a complete CI/CD pipeline with GitHub Actions for a TypeScript application.

Command used

/ops:ops-ci "Configure GitHub Actions with tests, lint, build and deployment"

Generated structure

.github/
└── workflows/
├── ci.yml # Tests and quality
├── cd.yml # Deployment
└── dependabot.yml # Dependency updates

CI Pipeline

.github/workflows/ci.yml

name: CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

# Cancel previous runs on the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
NODE_VERSION: '20'
PNPM_VERSION: '8'

jobs:
# ==========================================
# Job 1: Lint and formatting
# ==========================================
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run ESLint
run: pnpm lint

- name: Check formatting
run: pnpm format:check

- name: Type check
run: pnpm typecheck

# ==========================================
# Job 2: Unit tests
# ==========================================
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run migrations
run: pnpm db:migrate
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db

- name: Run tests
run: pnpm test:ci
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db
CI: true

- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: false

# ==========================================
# Job 3: Build
# ==========================================
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build
env:
NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: .next
retention-days: 7

# ==========================================
# Job 4: E2E tests (on PR only)
# ==========================================
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium

- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: build
path: .next

- name: Run E2E tests
run: pnpm test:e2e
env:
BASE_URL: http://localhost:3000

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7

# ==========================================
# Job 5: Security scan
# ==========================================
security:
name: Security Scan
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run npm audit
run: pnpm audit --audit-level=high
continue-on-error: true

- name: Run Snyk security scan
uses: snyk/actions/node@master
continue-on-error: true
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high

CD Pipeline

.github/workflows/cd.yml

name: CD

on:
push:
branches: [main]
tags:
- 'v*'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
required: true
default: 'staging'
type: choice
options:
- staging
- production

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
# ==========================================
# Job 1: Build and push Docker image
# ==========================================
build-image:
name: Build Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=

- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }}

- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}

# ==========================================
# Job 2: Deploy to Staging
# ==========================================
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build-image
if: github.ref == 'refs/heads/main' || github.event.inputs.environment == 'staging'
environment:
name: staging
url: https://staging.example.com

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Deploy to staging
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
cd /opt/myapp
docker pull ${{ needs.build-image.outputs.image-tag }}
docker-compose -f docker-compose.staging.yml up -d --force-recreate
docker system prune -f

- name: Health check
run: |
sleep 30
curl -f https://staging.example.com/api/health || exit 1

- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

# ==========================================
# Job 3: Deploy to Production
# ==========================================
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [build-image, deploy-staging]
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.environment == 'production'
environment:
name: production
url: https://www.example.com

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup kubectl
uses: azure/setup-kubectl@v3

- name: Configure kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config

- name: Deploy to Kubernetes
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-image.outputs.image-digest }} \
--namespace=production

- name: Wait for rollout
run: |
kubectl rollout status deployment/myapp --namespace=production --timeout=300s

- name: Health check
run: |
sleep 30
curl -f https://www.example.com/api/health || exit 1

- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Dependabot

.github/dependabot.yml

version: 2
updates:
# npm dependencies
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
groups:
dev-dependencies:
dependency-type: "development"
update-types:
- "minor"
- "patch"
production-dependencies:
dependency-type: "production"
update-types:
- "patch"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
commit-message:
prefix: "chore(deps)"

# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"

# Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(docker)"

package.json Scripts

{
"scripts": {
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"test": "jest",
"test:ci": "jest --ci --coverage --runInBand",
"test:e2e": "playwright test",
"build": "next build",
"db:migrate": "prisma migrate deploy",
"db:generate": "prisma generate"
}
}

Key points

AspectImplementation
ConcurrencyCancellation of previous runs
Cachepnpm, Docker layer cache (GHA)
Environmentsseparate staging / production
SecretsGitHub Secrets for credentials
NotificationsSlack webhooks
Securitynpm audit + Snyk + SBOM
  • /ops:ops-docker - Optimized Dockerfile
  • /ops:ops-k8s - Kubernetes manifests
  • /qa:qa-security - Full security audit

Branch protection

Enable branch protection for main:

  • Require status checks (lint, test, build)
  • Require pull request reviews
  • Require signed commits