Setting Up CI/CD Pipelines with GitHub Actions
Manual deployments are scary deployments. One wrong command and production is down. CI/CD removes the fear and adds consistency. Here's how to set it up properly.
What CI/CD Actually Means
CI (Continuous Integration)
- Run tests on every push
- Prevent broken code from merging
CD (Continuous Delivery/Deployment)
- Automate release process
- Deploy on merge to main
Your First Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
Multi-Stage Pipeline
name: CI/CD
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: dist/
# Deploy steps here
Environment Variables and Secrets
Store in GitHub: Settings → Secrets and Variables → Actions
jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_ENV: production
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: aws s3 sync dist/ s3://my-bucket
Deploy to Common Platforms
Vercel
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Docker + AWS ECR
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} $ECR_REGISTRY/myapp:${{ github.sha }}
docker push $ECR_REGISTRY/myapp:${{ github.sha }}
Caching for Speed
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-
Branch Protection
After setting up CI:
- Go to Settings → Branches → Add rule
- Branch name pattern:
main - Enable:
- Require status checks to pass
- Require branches to be up to date
- Select your test job
Workflow Tips
- Fail fast: Run linting before tests (faster feedback)
- Matrix builds: Test across Node versions
- Manual approval: Add environment protection for production
- Reusable workflows: Extract common steps
# Matrix example
jobs:
test:
strategy:
matrix:
node: [18, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
CI/CD is an investment that pays off immediately. Start simple, iterate as needed.