I Automated My Whole Deploy Pipeline with GitHub Actions (Took 2 Days)
I used to deploy manually. SSH into the server, pull the latest code, run the build script, cross my fingers, and hope nothing broke. It worked — until the day I pushed a typo to production at 11 PM on a Friday and didn't notice until Monday morning.
That was the last straw. I spent the next weekend building a proper CI/CD pipeline with GitHub Actions. Two days of setup. It's paid for itself a hundred times over.
Why GitHub Actions Instead of Jenkins?
I've used Jenkins before. It's powerful but you need to maintain a server, manage plugins, deal with Java issues. For a solo developer, that overhead kills productivity. GitHub Actions wins on simplicity — the runners are managed by GitHub, the YAML is straightforward, and the marketplace has pre-built actions for everything.
The free plan gives 2,000 minutes/month. My pipeline runs in ~4 minutes, so 500 runs/month before hitting limits.
The Pipeline Structure
I split it into three workflows:
- CI: Runs on every PR. Lint, test, build (~90s).
- CD: Runs when merged to main. Docker build, deploy.
- Nightly: 2 AM. Security scans, dependency updates.
The CI Workflow
name: CI
on:
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}Key decisions: use npm ci not npm install — it's faster and uses exact lockfile versions. Enable caching with cache: 'npm' — first run 60s, subsequent runs 5s.
The CD Workflow
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}The wrangler-action does the heavy lifting. Pass your API token as a secret, and it handles auth and deployment.
Slack Notifications
I added a Slack step so the team knows when deployments happen:
- name: Notify Slack
uses: slackapi/slack-github-action@v1.26.0
with:
payload: '{"channel":"#deployments","text":"🚀 Deployed ${{ github.repository }}"}'Comes in handy when something breaks and you need to trace which change caused it.
What I'd Do Differently
1. Start with caching earlier. First few months no caching meant 3+ min builds. Adding cache dropped it to 90s.
2. Add a staging environment. Everything goes straight to prod right now. Planning to add a manual approval gate.
3. Pin action versions. Using @v4 tags is risky — maintainers can push breaking changes. Pin to specific SHAs.
4. Matrix testing for Node versions. If your app supports multiple versions, test them all in parallel:
strategy:
matrix:
node-version: [18, 20, 22]The Cost Calculation
- GitHub Actions: Free (public) or $4/month (Team, 3,000 min)
- Cloudflare Workers: $0 (free tier) or $5/month
- Time saved: 30 min/deploy × 3-4/week = 6 hours/month
No more Friday night deployment anxiety. That alone is worth the setup.
Start Small
Don't copy-paste my whole pipeline. Start with 10 lines of YAML that just runs tests. Add complexity as you go. I almost gave up trying to build the perfect pipeline on day one.
What does your CI/CD pipeline look like? I'm always looking for ways to improve mine.