Thumbnail image for A Minimal GitHub Actions CI/CD Setup
August 9, 2026
engineering series
CICD
DevOps
GitHubActions

A Minimal GitHub Actions CI/CD Setup

Most CI/CD advice assumes a team. Here's the smallest pipeline that still catches real problems, and where the line is before you're overbuilding it.

Most CI/CD writing assumes a team.

Staging environments. Caching layers. Required reviewers. Matrix builds across runtime versions. None of that is wrong, exactly. It’s just built for a scale most personal and small-team projects don’t operate at.

The trap is that this is also the default shape of most tutorials and templates. Copy the recommended setup and you’ve imported a pipeline designed for a much bigger problem than the one you actually have.

This is the same shape of decision I laid out for my solo stack more broadly: CI/CD is one more place where the constraint isn’t flexibility, it’s how much you’re willing to maintain alone.

Where the default breaks

Every piece in a “proper” pipeline solves a real problem, somewhere.

Staging environments catch issues before they hit real users when many people are shipping into the same codebase at once. Caching saves meaningful time when a build takes minutes, not seconds. Matrix builds catch environment-specific breakage when your users span several platforms and runtime versions. Required reviewers slow down merges deliberately, because the cost of a bad merge is high enough to justify friction.

None of that is free. Each addition is one more thing that can fail, one more piece of YAML to understand months later, one more place a pipeline goes red for a reason that has nothing to do with your code.

For a solo project or a small team, most of these costs get paid without the matching benefit. You’re carrying infrastructure sized for a problem you don’t have.

What actually works

My pipelines stay small on purpose, the same way the rest of a lightweight architecture does: deployment through a straightforward pipeline, nothing layered on that isn’t earning its place.

Every check runs on the pull request: lint, test, build. Nothing merges to main without passing all three. That’s not optional, even on projects with just me committing. The PR gate exists so a broken change never sits on main, not because I don’t trust myself, but because the check catches the things I miss when I’m moving fast.

Deploy happens on merge to main, directly, no staging environment in between. The only exception is small experiments, thrown together on their own, where breaking something has no real cost. Anything that counts as a real project goes through the PR gate every time, no exceptions.

No caching. No matrix builds. No required reviewers beyond the checks themselves.

Here’s what that looks like as an actual workflow file, for an Astro-based static site deploying to GitHub Pages:

name: CI

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

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: npm run build
      - if: github.event_name == 'push'
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./dist

  deploy:
    needs: build
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4

One job runs on every pull request and every push: install, lint, test, build. A second job only runs on push to main, and only after the first job passes, publishing what the first job already built. Nothing merges broken, and nothing deploys without having already passed the same checks a pull request had to pass.

The caching question is a good example of where the line sits. Every GitHub Actions tutorial adds actions/cache for node_modules as a default. For a small Astro site, the full install and build already finishes in under a minute. Adding a cache step would save a few seconds and introduce a new failure mode: stale caches, invalidation bugs, a build that behaves differently depending on whether the cache hit. That’s a bad trade until build time is actually the bottleneck, not before.

When to add more

The mistake isn’t having a minimal pipeline. It’s assuming the minimal pipeline never changes.

That doesn’t mean the pipeline stays literally frozen forever. Blog subscriptions run as a small script inside the same CI flow, scanning frontmatter and sending notifications on deploy. That’s still a minimal pipeline. It’s an addition that solved a specific need without turning into new infrastructure to maintain.

A few honest, self-directed triggers for adding complexity:

Build time crossing a few minutes and starting to block iteration is when caching earns its place. Not before. More than one deployable thing needing to move together, an API and a frontend, or coordinated migrations, is when a single linear workflow stops being enough. Someone other than you needing to trust the pipeline without asking you first, meaning the team has grown past the point where tribal knowledge about what’s safe to merge can stay implicit, is when required reviewers stop being overhead and start being the actual point.

There’s a second category worth naming separately: triggers that come from outside pressure rather than internal need. A client or compliance requirement demanding an approval gate isn’t the system telling you it’s outgrown itself. It’s an external party requiring something, which is a different reason to add it, and one worth recognizing as such rather than absorbing quietly into “best practice.”

The distinction matters. Add complexity when the pipeline demands it. Not because a tutorial said so, and not because it looks more serious.

Closing

A minimal pipeline isn’t a lesser version of a proper one. It’s the version sized to the problem you actually have.

PR-gated checks, a direct deploy on merge, nothing preemptive. It stays that way until the pipeline itself makes the case for more. That’s the same principle behind keeping systems small over long horizons and deciding what not to build: the cost of an addition is easy to justify in the moment and expensive to carry later, so the addition should wait until it’s earned.

If you're still here, might as well subscribe :)
Get notified via

Related Articles