Skip to main content
Back to blog

Setting up CI/CD with GitHub Actions

·3 min readDeveloper Tools

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 test

Every 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

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS