All Experiments
╔══════════════════════════════════════════════════════════════╗
║  EXPERIMENT: CI/CD Pipeline Migration                        ║
║  KEY METRIC: Caught real bug in 30 min, $0 cost              ║
║  workway.co                                                   ║
╚══════════════════════════════════════════════════════════════╝
ValidatedJanuary 29, 2026

CI/CD Pipeline Migration

We moved from GitHub Actions to Cloudflare Workers Builds. It took about an hour. The pipeline now costs nothing, deploys faster, and—here's the thing—it actually works.

The Situation

Our GitHub Actions pipeline had developed an unfortunate habit of not running. The error message was refreshingly honest:

The job was not started because recent account payments have failed 
or your spending limit needs to be increased.

This is the kind of problem that doesn't announce itself. You push code, you see the green checkmark from a previous run, you assume everything's fine. Meanwhile, your CI hasn't actually validated anything in days.

We were already running on Cloudflare for everything else—Workers, D1, KV, the works. GitHub Actions was the odd one out, a separate billing relationship with a separate set of credentials, building code in Virginia that would ultimately run on Cloudflare's edge.

What We Learned

The pipeline caught a real bug

Within thirty minutes of going live, the new pipeline blocked a deployment. A file was missing an import:

// Before: sql not imported
newsletterIssues.sentAt ? sql`...` : sql`...`

// Error:
// src/routes/newsletter.ts(1813,31): error TS2304: Cannot find name 'sql'.

This would have been a runtime error in production. Instead, it was a failed build that took two minutes to fix. Our previous pipeline, sitting idle because of a billing dispute, would have let this through.

Build times are reasonable

Initialize2-3s
Clone4-6s
Install25-35s
Build/Lint20-80s
Deploy15-25s
Total~1-2 min

Not instantaneous, but fast enough that you don't context-switch while waiting.

The economics work

GitHub ActionsWorkers Builds
Free tier2,000 min/mo3,000 min/mo
When exceededBlocked$0.005/min
BillingSeparateSame as hosting

We're unlikely to hit 3,000 minutes. But if we do, the failure mode is "costs a few dollars" rather than "silently stops working."

Is This Technical Debt?

We asked ourselves: are we solving a problem, or deferring it?

Arguments for "this is debt"

  • • No automated tests in CI
  • • No native approval workflows
  • • More locked into Cloudflare

Arguments for "this is an improvement"

  • • One fewer external dependency
  • • Build env matches production
  • • Predictable billing model

Our take: It's an improvement. The trade-offs are real but manageable. We can always add a self-hosted GitHub runner later if we need test automation. For now, TypeScript catches most of what matters, and the pipeline actually runs.

The Zuhandenheit Test

There's this concept from Heidegger—zuhandenheit, or "ready-to-hand." A tool that works well becomes invisible. You don't think about the hammer; you think about the nail.

Before

We thought about our CI constantly. Is it running? Did the billing go through? Why is this still showing the old check?

After

Push code. It appears on workway.co. The pipeline isn't something we think about anymore.

That's the goal. Not a sophisticated pipeline with twenty integrations—just one that fades into the background because it reliably does its job.

Verdict: Improvement

The migration simplifies our infrastructure by removing a billing-sensitive dependency and aligning our CI/CD with our deployment target. The pipeline caught a real bug within 30 minutes of going live. The tool recedes.