CI/CD Best Practices — Quality Gates, Reporting and Pipeline Optimisation

Running Cypress in CI is the foundation. Making it a reliable quality gate that the team trusts is the goal. This lesson covers the practices that transform a CI pipeline from “tests run somewhere” to “every merge is protected by automated quality verification”: PR-blocking status checks, smart test selection for faster feedback, artifact management, timeout tuning, and pipeline performance monitoring.

Production CI/CD Best Practices for Cypress

These practices come from teams running Cypress in production pipelines with 200+ tests, multiple daily deployments, and strict quality requirements.

// ── PRACTICE 1: PR-blocking quality gate ──

/*
  GitHub: Settings → Branches → Branch protection rules
    - Require status checks: "Cypress Tests" must pass before merging
    - Require branches to be up to date before merging

  Effect: PRs with failing Cypress tests CANNOT be merged to main.
  This is the single most important CI practice — it makes test failures
  impossible to ignore and ensures every merge is quality-verified.
*/


// ── PRACTICE 2: Two-tier test execution ──

const TWO_TIER_STRATEGY = {
  'Tier 1 — Smoke (every PR push)': {
    tests: 'Critical path only — login, checkout, core CRUD',
    count: '20-30 tests',
    duration: '2-3 minutes',
    purpose: 'Fast feedback on every commit; blocks PR merge on failure',
    config: 'npx cypress run --spec "cypress/e2e/smoke/**"',
  },
  'Tier 2 — Full regression (merge to main / nightly)': {
    tests: 'Complete test suite — all features, edge cases, cross-browser',
    count: '150-200 tests',
    duration: '10-15 minutes (with parallelisation)',
    purpose: 'Comprehensive coverage before deployment',
    config: 'npx cypress run --record --parallel',
  },
};


// ── PRACTICE 3: Smart test selection (affected tests only) ──

/*
  When a PR changes only the checkout component, running all 200 tests
  wastes 12 minutes on unrelated tests. Smart selection runs only the
  specs that test the changed code.

  Approaches:
  1. Tag-based: @smoke, @checkout, @auth tags → filter by tag
     npx cypress run --env grepTags=checkout

  2. File-based: Map changed source files to relevant spec files
     If src/components/Checkout/ changed → run cypress/e2e/checkout/*.cy.ts

  3. Cypress Cloud Smart Orchestration:
     Automatically prioritises specs that historically fail for the changed code
*/


// ── PRACTICE 4: Artifact and evidence management ──

const ARTIFACT_STRATEGY = {
  'Screenshots': {
    when: 'On failure only (screenshotOnRunFailure: true)',
    where: 'cypress/screenshots/',
    upload: 'Always upload with if: always() or when: always',
    retention: '7 days (most failures are diagnosed within 24 hours)',
  },
  'Videos': {
    when: 'Disabled by default in CI (video: false) — enable for full regression',
    where: 'cypress/videos/',
    upload: 'Only on failure to save storage',
    retention: '3 days',
  },
  'JUnit XML': {
    when: 'Every run — for CI dashboard integration',
    where: 'results/cypress-results.xml',
    upload: 'Always — CI platforms parse JUnit for test result display',
    config: 'reporter: junit, reporterOptions: { mochaFile: "results/[hash].xml" }',
  },
  'Mochawesome HTML': {
    when: 'Full regression runs — rich HTML report with embedded screenshots',
    where: 'reports/mochawesome.html',
    upload: 'Always — shareable with non-technical stakeholders',
    config: 'npm install mochawesome mochawesome-merge mochawesome-report-generator',
  },
};


// ── PRACTICE 5: Timeout and retry tuning for CI ──

const CI_TUNING = {
  defaultCommandTimeout: '10000 (10s — up from 4s default, CI is slower)',
  pageLoadTimeout: '60000 (60s — slow CI networks)',
  responseTimeout: '30000 (30s — API calls in CI may be slower)',
  retries_runMode: '2 (retry twice in CI to handle transient failures)',
  retries_openMode: '0 (no retries locally — see failures immediately)',
  video: 'false (disable by default — enable only for full regression)',
  screenshotOnRunFailure: 'true (always capture failure evidence)',
};


// ── PRACTICE 6: Pipeline performance monitoring ──

const PERFORMANCE_TARGETS = [
    {
        metric: 'Smoke suite (PR check)',
        target: '< 5 minutes',
        action: 'If exceeded: reduce smoke test count or increase parallelism',
    },
    {
        metric: 'Full regression',
        target: '< 15 minutes',
        action: 'If exceeded: increase parallelism, split slow specs, optimise setup',
    },
    {
        metric: 'Flake rate',
        target: '< 2% of test runs involve retries',
        action: 'If exceeded: fix top 5 flaky tests; investigate environment stability',
    },
    {
        metric: 'Pipeline reliability',
        target: '< 1% infrastructure failures (non-test related)',
        action: 'If exceeded: improve Docker image pinning, dependency caching, network stability',
    },
];

console.log('CI/CD Best Practices Summary:');
console.log('  1. PR-blocking: Cypress status check required for merge');
console.log('  2. Two-tier: Smoke on every push, full regression on merge/nightly');
console.log('  3. Smart selection: Run only affected tests when possible');
console.log('  4. Artifacts: Screenshots + JUnit XML on every run; videos on failure');
console.log('  5. CI tuning: Higher timeouts, retries in run mode only');
console.log('  6. Monitoring: Track suite duration, flake rate, pipeline reliability');
Note: The two-tier strategy is the most impactful CI practice for team productivity. A 3-minute smoke suite on every PR push gives developers fast feedback on critical paths. A 15-minute full regression on merge to main or nightly catches everything else. Without this split, every push triggers a 15-minute full regression — developers wait or context-switch, slowing the entire team. With the split, most pushes get a 3-minute check; full regression runs when it matters most (before deployment).
Tip: Generate JUnit XML reports (reporter: 'junit' in cypress.config) for every CI run. Most CI platforms (GitHub Actions, GitLab, Jenkins) natively parse JUnit XML and display test results in their UI — individual test names, pass/fail status, durations, and error messages appear directly in the pipeline dashboard without opening Cypress Cloud. This gives immediate visibility without requiring a separate analytics platform.
Warning: Setting retries too high in CI masks real defects behind successful retries. A test with retries: 5 might pass on the 5th attempt every run, making the pipeline green while hiding a genuine timing bug. Keep retries at 1-2 maximum. If a test needs more than 2 retries to pass consistently, it has a root cause that must be fixed — not a transient issue that retries can handle.

Common Mistakes

Mistake 1 — Not making Cypress a required PR status check

❌ Wrong: Cypress tests run in CI but are advisory — developers can merge PRs even when tests fail, defeating the purpose of automated quality gates.

✅ Correct: Configuring branch protection rules to require the Cypress status check before merging. Failed tests block the PR until they are fixed or the failure is explained.

Mistake 2 — Running the full test suite on every push

❌ Wrong: Every push triggers 200 tests taking 15 minutes — developers push 10 times a day, consuming 150 minutes of CI time and delaying feedback.

✅ Correct: Smoke tests (30 critical tests, 3 minutes) on every push. Full regression (200 tests) on merge to main and nightly. 90% faster feedback for 90% of pushes.

🧠 Test Yourself

A team runs their full 200-test Cypress suite on every PR push, taking 15 minutes. Developers complain about slow feedback. What is the best optimisation?