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.
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.
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 }}
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-
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.
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.
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.
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.