Setting Up CI/CD With GitHub Actions: The Pipeline That Finally Stuck

By James Nguyen Updated September 24, 2026
Setting Up CI/CD With GitHub Actions: The Pipeline That Finally Stuck

I'd built three different CI pipelines at three different jobs before this one, and every single one eventually rotted, tests got skipped when they were flaky, deploys became manual "just to be safe" during a busy sprint, and within a year the pipeline was more decoration than actual gatekeeper. The GitHub Actions setup I built for my current team is the first one that's survived over a year of real use without anyone quietly bypassing it, and I think I finally understand why the earlier ones failed.

The earlier pipelines died from being too slow, not too weak

Looking back, my previous pipelines had reasonable test coverage, they died because a full run took twelve to fifteen minutes, long enough that people started merging without waiting, or clicking "skip checks" during a deadline crunch, and once that habit starts, the pipeline stops actually gating anything. I built this version with speed as a primary design constraint from day one, not an afterthought.

Splitting the test suite into parallel jobs cut wall-clock time dramatically

Instead of one job running the entire test suite sequentially, I split tests across four parallel jobs by directory, unit tests, integration tests, and two separate chunks of end-to-end tests, all running simultaneously rather than one after another. Total wall-clock time dropped from around thirteen minutes to under four.

jobs:
  test:
    strategy:
      matrix:
        suite: [unit, integration, e2e-part1, e2e-part2]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run test:${{ matrix.suite }}

Caching dependencies was a bigger win than I expected

Before adding dependency caching, every single run did a full npm install from scratch, taking close to ninety seconds on its own before a single test even ran. Caching node_modules based on a hash of the lockfile cut that down to around eight seconds on a cache hit, which across dozens of daily pipeline runs adds up to real time saved.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('package-lock.json') }}
    restore-keys: npm-

The flaky test problem I solved by actually fixing flaky tests, not hiding them

My earlier pipelines dealt with flaky tests by letting people re-run failed jobs until they passed, which quietly trained everyone to ignore red builds as probably-flaky rather than probably-broken. This time, I tracked every flaky test in a spreadsheet for a month, and genuinely fixed the underlying timing issues, mostly tests that didn't properly wait for async operations, rather than tolerating retries as a permanent workaround. Flaky retries are now explicitly the exception, not the default behavior.

Branch protection rules that actually have teeth

The pipeline itself doesn't matter if it's trivially bypassable, so I enabled branch protection requiring the full check suite to pass, with no exceptions available even to repository admins, a setting I specifically had to argue for since a couple of senior engineers initially wanted an admin override "for emergencies." We've had exactly one genuine emergency in the year since, and the pipeline caught a real bug during it, which settled that argument permanently.

Deploy previews for every pull request changed how people reviewed code

Beyond just running tests, the pipeline deploys every pull request to a temporary preview environment automatically, meaning reviewers can click a link and actually interact with the change rather than reading a diff and trusting it works. This single addition noticeably improved the quality of code review comments on our team, since people started catching UX issues that a pure code read would have missed entirely.

What's actually kept this one alive for over a year

Speed, genuine flaky test fixes instead of tolerance, and branch protection with no escape hatch are the three specific things I didn't get right in my earlier attempts. A pipeline people can bypass eventually gets bypassed, and a pipeline that's slow enough to resent eventually gets resented into irrelevance. This one is neither, and it's the first CI setup in my career that's still doing its actual job a year later rather than quietly rotting into ceremony.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles