Setting Up a Production-Grade CI/CD Pipeline With GitHub Actions
A good CI/CD pipeline catches bugs before they reach production, deploys changes safely, and gives developers confidence that their code works. A bad pipeline is so slow or flaky that developers ignore it, merge without waiting for checks, and deploy broken code.
I have set up CI/CD pipelines at six companies. The pattern is always the same: start minimal, add checks as you need them, and ruthlessly eliminate flakiness. Here is the end state you should aim for.
The Pipeline Architecture
A production-grade pipeline has four stages:
Validation (runs on every push): lint, type check, unit tests. Fast feedback (under 2 minutes). Catches syntax errors, type mismatches, and broken tests immediately.
Integration (runs on PRs): integration tests, build verification, preview deployment. Medium feedback (2-5 minutes). Ensures the application builds correctly and key features work.
Deployment (runs on merge to main): build, deploy to production, smoke tests. Takes the longest (3-10 minutes) but only runs on proven code.
Post-deploy (runs after deployment): smoke tests against production, monitoring check, notification. Verifies the deployment is healthy.
Stage 1: Validation
This runs on every push to every branch. It must be fast (under 2 minutes) so developers get immediate feedback.
Linting: ESLint catches code quality issues, unused variables, and potential bugs. Configure it once and enforce it in CI. No more "you forgot to remove that console.log" in code reviews.
Type checking: TypeScript compiler (tsc --noEmit) verifies type safety across the entire codebase. Catches type errors that your editor might miss (especially in files you did not open).
Unit tests: run all unit and integration tests. With Vitest, 400+ tests complete in under 10 seconds. Use the --reporter=github option to annotate the PR with test failures inline.
Format checking: Prettier with --check verifies all files are formatted correctly. This eliminates formatting debates in code reviews — the CI enforces it.
The key: all four checks run in parallel. Linting, type checking, testing, and format checking are independent. Running them in parallel means total time is the duration of the slowest check, not the sum of all checks.
Stage 2: Integration (PR Checks)
This runs when a PR is opened or updated. It can take longer because developers are not waiting at their keyboard for immediate feedback.
Build verification: npm run build must succeed. Catches issues that only appear during the build (import errors, missing environment variables, SSR incompatibilities). This is the check that catches "it works in dev but breaks in production."
Integration tests: test critical user flows with a real browser (Playwright). On a PR that changes the tool UI, run the tool-related E2E tests. On a PR that changes auth, run the auth flow tests.
Preview deployment: deploy the PR to a unique preview URL. This lets reviewers (and QA, and product managers) see the changes live without checking out the branch locally. Vercel does this automatically. For custom setups, deploy to a PR-specific subdomain.
Bundle size check: compare the production bundle size against the base branch. Alert if the bundle grew by more than 5%. This catches accidental imports of large libraries.
Stage 3: Deployment
This runs when code merges to main. The code has already passed validation and integration checks, so this stage focuses on getting it to production safely.
Build: compile the production application. For Next.js, this is next build. The output is a deployable artifact.
Deploy: push the artifact to your hosting platform. For Vercel, this is automatic. For custom deployments, push a Docker image to a registry and trigger a rolling update.
Database migrations: if the PR includes a Prisma migration, run it against the production database. This should happen BEFORE the new code deploys (expand-contract pattern) to avoid the new code expecting a schema that does not exist yet.
Smoke tests: after deployment, run 5-10 critical E2E tests against the production URL. These verify: the homepage loads, a tool page works, the API responds, authentication works. If any fail, automatically roll back.
Stage 4: Post-Deploy
Notification: send a message to your team channel with what deployed (PR title, author, link to changes). This gives everyone visibility into production changes.
Monitoring watch: for the 10 minutes after deployment, watch error rates in Sentry. If new errors spike above a threshold, alert the team. Optionally, automate a rollback.
GitHub Actions Workflow Structure
Organize workflows by trigger:
ci.yml: triggered on push to any branch. Runs validation (lint, type check, test, format check). Fast, runs on every push.
pr.yml: triggered on pull_request. Runs build verification, integration tests, bundle size check, and preview deployment. Runs in addition to ci.yml on PRs.
deploy.yml: triggered on push to main. Runs the full build and deployment pipeline. Only runs after validation succeeds (GitHub's built-in status checks enforce this).
Caching for Speed
GitHub Actions caching makes a dramatic difference in pipeline speed:
Cache node_modules (or the pnpm store) keyed on the lock file hash. If dependencies have not changed, skip the install step entirely (saves 30-60 seconds).
Cache the Next.js build cache (.next/cache). Incremental builds only recompile changed pages (saves 30-90 seconds on builds).
Cache Playwright browsers. Browser binaries are large (200MB+). Cache them to avoid downloading on every run (saves 20-40 seconds).
With proper caching, our validation pipeline runs in 45 seconds. Without caching, it takes 3 minutes. That difference matters because developers wait for the first push's results.
Handling Flaky Tests
Flaky tests (tests that sometimes pass and sometimes fail without code changes) are the biggest threat to CI trust. When developers see a red check and know it is "just a flaky test," they start ignoring all red checks.
Prevention: avoid timing-dependent assertions (waitForTimeout), mock unstable external services, use test isolation (each test is independent), and avoid shared mutable state between tests.
Detection: track test failure rates over time. A test that fails 5% of the time is flaky. Quarantine it (move to a separate, non-blocking job) until fixed.
Mitigation: retry failed tests once automatically. If a test fails then passes on retry, it is flaky — flag it for investigation. Do not retry indefinitely; this masks real failures.
Our policy: a flaky test gets 48 hours to be fixed. After that, it is deleted. A deleted test is better than a flaky test because a flaky test teaches the team to ignore CI failures.
Secrets and Environment Variables
Store sensitive values (API keys, deployment tokens) in GitHub's encrypted secrets. Reference them in workflows with the secrets context. Never hardcode secrets in workflow files.
For preview deployments, use separate API keys (not production keys). Preview environments should connect to staging databases and staging third-party services.
Rotate CI secrets periodically. The GITHUB_TOKEN is automatic, but custom secrets (deployment keys, API tokens) should be rotated every 90 days.
Monorepo Considerations
In a monorepo, running all checks for every change wastes time. Use path filtering:
Only run tool tests when packages/tools/ changes. Only run web tests when apps/web/ changes. Only deploy the web app when its dependencies change.
GitHub Actions supports path filters natively: on: push: paths: ['apps/web/', 'packages/shared/']. Turborepo's --filter flag runs only affected packages.
Cost Optimization
GitHub Actions bills by the minute for private repositories. Reduce costs:
Use caching aggressively (fewer minutes per run). Use path filters (fewer runs). Use ubuntu-latest (Linux runners are cheapest). Cancel previous runs when a new push arrives (concurrency group with cancel-in-progress: true).
For open source projects (public repositories): GitHub Actions is free. No cost optimization needed.
Our Pipeline
AIDToolStack's pipeline:
We deploy 5-10 times per day. The pipeline catches 2-3 issues per week that would have reached production without CI. The ROI is excellent — 6 minutes of automation per deploy prevents hours of incident response.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.