← Back to blog Khalil Drissi

How to set up CI/CD with GitHub Actions

Listen to article
0:00

GitHub Actions gets a bad reputation because people copy a giant YAML file from a blog post, it half works, and they never touch it again until it breaks. I want to show you the small, understandable version that I actually run on real projects.

Where workflows live

Every workflow is a YAML file inside .github/workflows. The directory name is not optional. Each file describes one or more jobs, and each job runs on a fresh virtual machine. The mental model that helped me most: a job is a clean laptop that boots, does exactly what you tell it, and then evaporates. Nothing persists between jobs unless you explicitly save it.

A minimal but real pipeline

Here is a workflow that runs on every push and pull request. It installs dependencies, runs the test suite, and builds the site. Note how the trigger, the runner, and the steps map to plain English:

name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    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
      - run: npm run build

Two things here earn their keep. The cache: npm line restores your dependency cache so installs drop from a minute to a few seconds. And npm ci instead of npm install respects your lockfile exactly, which means CI installs the same versions every time. Reproducibility is the whole point.

Run jobs in parallel when you can

If your lint, test, and type-check steps do not depend on each other, do not chain them. Split them into separate jobs and they run at the same time on different machines. Your feedback loop gets shorter, and the cost is the same since you pay for compute minutes either way. I only force sequence with needs when a later job genuinely requires an earlier one to finish, like deploy waiting on test.

Adding deployment safely

This is where I see the most mistakes. Deployment should only run on the main branch, never on pull requests, and it should depend on tests passing. The shape looks like this:

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - name: Publish
        run: npx wrangler pages deploy dist --project-name my-site

Wrangler needs a Cloudflare API token to publish. Store it as an encrypted repository secret under Settings, Secrets and variables, and expose it to the step through the job’s environment block. Never paste a token into the YAML itself, because the file is in your history forever. If you are deploying to Cloudflare specifically, the manual setup side is covered in deploying a static site to Cloudflare Pages.

Secrets, the right way

Repository secrets are encrypted and only decrypted at runtime inside the job. They are masked in logs, so if a token accidentally prints, GitHub redacts it. Reference them through the secrets context in your workflow’s environment mapping rather than echoing them. The golden rule: if it would let someone deploy or spend money, it is a secret, and it lives in GitHub’s secret store, not your code.

Caching beyond dependencies

You can cache more than node_modules. Build artifacts, compiled binaries, downloaded assets, anything expensive to recreate is a candidate. The actions/cache step takes a key, usually a hash of a lockfile or source directory, and restores the matching cache if it exists. Get the key right and your slow steps become instant. Get it wrong and you ship stale artifacts, so always include the relevant file hash in the key.

Keep it boring

My strongest advice is to resist cleverness. A workflow that anyone on the team can read in thirty seconds is worth more than a brilliant one only you understand. Add steps when you have a concrete reason, delete them the moment they stop pulling their weight, and pin your action versions so a surprise update never breaks a Friday deploy. If your pipeline also publishes content that needs to be searchable, you can fold that step in too, which pairs nicely with adding full-text search to a static site. Done well, CI/CD fades into the background and just keeps your main branch deployable.

Comments
Leave a comment