Setting up CI/CD with GitHub Actions
Before CI/CD, my deployment process was: SSH into the server, pull the latest code, rebuild, restart the service, and hope nothing broke. Now I push to main and everything happens automatically. GitHub Actions made this trivially easy to set up.
What GitHub Actions does
GitHub Actions runs workflows in response to events (push, pull request, schedule). Each workflow is a YAML file in .github/workflows/ that defines jobs with steps. Steps can run shell commands, use community-maintained actions, or execute scripts.
A basic CI workflow
This runs your tests on every push and pull request:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- run: pnpm install
- run: pnpm lint
- run: pnpm build
- run: pnpm testEvery push triggers a clean install, lint, build, and test. If any step fails, the workflow fails and you know before merging.
Adding deployment
Extend the workflow to deploy after tests pass:
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- run: pnpm install
- run: pnpm build
- name: Deploy to server
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
rsync -avz --delete -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
./dist/ deploy@your-server:/var/www/app/The needs: test ensures deployment only runs after tests pass. The if condition restricts deployment to pushes to main (not pull requests).
Secrets
Store sensitive values (SSH keys, API tokens, passwords) in GitHub repository settings under Secrets. Reference them in workflows with ${{ secrets.SECRET_NAME }}. They are never logged in workflow output.
Caching
The cache: "pnpm" option in the Node.js setup action caches the pnpm store between runs. This cuts install time from 30+ seconds to about 5 seconds on subsequent runs.
Matrix builds
Test across multiple Node versions or operating systems:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}What I automate
Every project I maintain has at minimum a CI workflow that lints and builds on push. For deployed projects, I add automatic deployment to the server or hosting platform. For libraries, I add automated npm publishing on tagged releases.
The initial setup takes 10-15 minutes per project. The time saved from not manually deploying (and not deploying broken code) pays for itself almost immediately.
Sources
Related posts
Why shadcn/ui changed how I build React interfaces
What makes shadcn/ui different from traditional component libraries, and why copying components into your project is actually the better approach.
pnpm workspaces: managing monorepos without the headache
A practical guide to using pnpm workspaces for monorepo management, with real patterns from actual projects.
Building a personal knowledge base with Obsidian
How I use Obsidian to organize notes, documentation, and ideas with linked thinking and plain Markdown files.
Enjoying the blog? Subscribe via RSS to get new posts in your reader.
Subscribe via RSS