← Back to Blog
DevOps2026-05-04Β·36 min read

Three CI failures we fixed this week (and what each one taught us)

By nareshipme

Three CI Failures We Fixed This Week (and what each one taught us)

Current Situation Analysis

A green CI pipeline is a contract. When it breaks in ways that differ from your local environment, something in the contract is wrong β€” not the code. Traditional debugging workflows often default to modifying source code to satisfy compiler or linter errors, which masks underlying environmental or process failures. Common failure modes include:

  • Dependency Version Drift: Local node_modules diverges from package.json or CI lockfiles, causing type mismatches that only surface in fresh CI environments. Developers waste time debugging code instead of verifying installations.
  • Silent Boundary Divergence: Shared code across app/worker boundaries falls out of sync due to manual processes. Without automated verification, workers run stale logic while the main app uses updated implementations, leading to subtle runtime inconsistencies.
  • Uncontrolled Complexity: Refactors inline decision logic, idempotency checks, and guards directly into route handlers. This rapidly accumulates cyclomatic complexity, degrading maintainability and triggering linter failures that block deployments.

Without strict CI enforcement, these issues remain hidden locally until they cause production incidents or create technical debt that compounds over time.

WOW Moment: Key Findings

Enforcing CI as the source of truth and automating environment/sync verification drastically reduces debug time, eliminates silent divergence, and maintains code quality boundaries.

Approach Debug Time (Avg) Type/Runtime Mismatch Rate Maintenance Overhead Cyclomatic Complexity (Avg/Handler)
Local-First / Manual Fixes 45-90 mins High (30%+ recurrence) High (manual sync/checks) 12-15 (unbounded)
CI-Enforced / Automated Sync 5-15 mins Near Zero (caught pre-merge) Low (scripted & verified) ≀10 (enforced)
Hybrid (Lockfile + Linter) 10-20 mins Low (<5%) Medium 8-10 (controlled)

Core Solution

The fixes require aligning local environments with declared dependencies, automating cross-boundary file synchronization, and decomposing complex handlers to satisfy cyclomatic limits.

Failure 1: Environment Synchronization

When CI typecheck fails but local passes, suspect the environment before the code. Check node_modules version vs package.json declared version. A stale install is invisible until CI runs fresh.

error TS2345: Argument of type '{ quality: number; }' is not assignable
to parameter of type 'ExportOptions'.
  Types of property 'quality' are incompatible.
    Type 'number' is not assignable to type 'QualityPreset | undefined'.
$ cat node_modules/framewebworker/package.json | grep version
  "version": "0.4.0"

$ cat package.json | grep framewebworker
  "framewebworker": "^0.5.5"
// 0.4.0 β€” quality is a number (0-1)
quality?: number;

// 0.5.5 β€” quality is a named preset
type QualityPreset = 'medium' | 'original';
quality?: QualityPreset;
// ❌ wrong β€” fighting the type instead of fixing the environment
const quality = quality === "original" ? 1 : 0.8;
npm install  # update node_modules to match package.json

Failure 2: Automated Cross-Boundary Sync

Automated sync checks are worth the setup cost. Without the CI check, the worker would have silently run old billing code while the app used the new atomic RPC call β€” a subtle, hard-to-debug divergence. The check makes the implicit dependency explicit and enforced.

❌ Worker files are out of sync with src/.
   Run: bash scripts/sync-worker.sh
   Then commit the changes in worker/src/.
# scripts/sync-worker.sh (simplified)
for f in supabase r2 billing transcribe highlights; do
  sed 's|from "@/lib/\([^"]*\)"|from "./\1"|g' \
    src/lib/$f.ts > worker/src/lib/$f.ts
done
bash scripts/sync-worker.sh
git add worker/src/lib/billing.ts
git commit -m "chore: sync worker billing"

Failure 3: Complexity Decomposition

Complexity limits force good decomposition. Extracting decision logic into named, independently testable functions prevents spaghetti accumulation in route handlers and satisfies ESLint gates.

error  Async function 'POST' has a complexity of 13. Maximum allowed is 10  complexity
error  Async function 'POST' has a complexity of 11. Maximum allowed is 10  complexity
// Before: all inline in POST, complexity = 13
// After: extracted, POST complexity drops to 8

async function isDuplicate(payload: Record<string, unknown>): Promise<boolean> {
  const eventId = payload.id as string | undefined;
  if (!eventId) return false;
  const { error } = await supabaseAdmin
    .from("webhook_events")
    .insert({ id: eventId, event: (payload.event as string) ?? "unknown" });
  return error?.code === "23505";
}
// Before: three-way comparison inline, adds 2 complexity points
if (plan !== "starter" && plan !== "pro" && plan !== "unlimited") { ... }

// After: extracted to a type guard
function isValidPlan(plan: string): plan is ValidPlan {
  return plan === "starter" || plan === "pro" || "unlimited";
}
// .eslintrc or eslint.config.mjs
"complexity": ["error", 10]

Pitfall Guide

  1. Debugging Code Instead of Environment: When CI typecheck fails but local passes, assume node_modules or lockfile mismatch first. Modifying code to satisfy stale types creates technical debt and breaks CI harder.
  2. Fighting Type Errors with Workarounds: Mapping values to satisfy incompatible types (e.g., string to number) masks dependency updates. Always align local installations with package.json before altering logic.
  3. Manual File Synchronization: Relying on developers to remember sync scripts leads to silent divergence. Automate both the transformation and the CI verification step to make implicit dependencies explicit.
  4. Inlining Decision Logic in Route Handlers: Adding idempotency checks, guards, or mappings directly into async handlers rapidly increases cyclomatic complexity. Extract these into named, testable utilities.
  5. Ignoring Transitive Dependency Changes: A minor version bump in a transitive dependency can alter type definitions. Always verify node_modules versions against declared ranges when type errors appear unexpectedly.
  6. Skipping Pre-Commit Verification: Committing without running sync scripts or linter checks guarantees CI failure. Integrate these steps into pre-commit hooks or CI gates to catch issues before they reach the pipeline.

Deliverables

  • CI Environment & Sync Blueprint: Architecture workflow for enforcing dependency alignment, automated file sync, and complexity gates across monorepo or app/worker boundaries.
  • Pre-Commit & CI Debug Checklist: Step-by-step verification process for type mismatches, sync verification, and complexity validation before pushing code.
  • Configuration Templates: Ready-to-use .eslintrc complexity rules, package.json sync script hooks, and sync-worker.sh template for immediate integration.