CI Tells the Truth

·

·

4 min read

·

14 views

I decided to move away from having my lftcrwd application version-controlled locally to version-controlled on GitHub. This meant an opporunity to explore CI and GitHub Actions, as well as a security layer. (Semgrep)

I spent an afternoon setting up CI/CD for lftcrwd and broke everything. Which is the point, it turns out.

The setup was simple: connect the GitHub repo to Vercel so every push to main triggers an automatic build and deploy, then add a GitHub Actions pipeline to run a build check and a Semgrep security scan. Shouldn’t take long.

Local machine     
↓  
git pushGitHub (source of truth)     
↓  
triggers simultaneouslyGitHub Actions (runs CI — lint, build check, Semgrep scan)     
↓  
also triggersVercel (pulls code from GitHub, builds, deploys)

Fourteen issues later, I had learned more about how this stack actually works than I had in the previous month of building it.

The core problem I didn’t know I had.

Before this, I deployed with npx vercel –prod from my local machine. That uploads whatever’s on disk, committed to git or not. The app worked, looked fine, and shipped features. All good.

Except it wasn’t. The repo didn’t match production. Fixes I made locally never got committed. Components existed that were never wired into routes. Types were wrong. The whole thing was held together because nothing was checking it.

The moment GitHub became the source of truth, CI built from the repo — and the repo was a mess.

npm ci is not npm install.

First failure: npm ci refused to run because the lockfile was out of sync with package.json. I had added packages locally without committing the updated package-lock.json.

npm install is forgiving; it updates the lockfile and moves on. npm ci treats the lockfile as a contract. If they don’t match exactly, it stops. This is intentional. You want reproducible installs in CI, the same packages every time, no surprises.

The fix was running npm install locally and committing the result. Easy. But I hadn’t understood the distinction before.

Turbopack found 40+ bugs I didn’t know existed.

The bigger one: my app had been building with webpack locally. CI used Turbopack — Next.js 16’s default. Turbopack is stricter.

The app started as a gym-matching platform and evolved into a coaching app. lib/supabase.ts, the file with all the types and database helpers, was never updated. It still had the old types. Pages imported functions that didn’t exist. Types were wrong or missing entirely.

Webpack let it slide. Turbopack did not.

Forty-plus TypeScript errors, all real and could have caused runtime failures. I fixed them one by one: missing fields on types, wrong return types, nullable values called without null checks. Each was a small bug that had been hiding.

CI didn’t create these problems. It just found them.

Git discipline is not optional.

The part that embarrassed me most: I’d fix an error, CI would run again, and the same error would fail twice.

I was editing files locally and not committing them. CI builds from git. Anything not committed doesn’t exist to CI. git log — components/DailyChecklist.tsx showed only the initial commit; the fix I applied was unstaged on my machine.

Run git status before you push every time. This should be obvious, but I’m writing it down anyway.

Semgrep no longer has a v1 action.

Minor thing but worth noting: the Semgrep GitHub Action (semgrep/semgrep-app@v1) is deprecated. The action I found in their docs didn’t exist. Neither did semgrep/semgrep@v1.

The current approach is to run their Docker container directly in the CI job:

container:  image: semgrep/semgrepsteps:  - uses: actions/checkout@v4  - run: semgrep ci

Pull the image and run the CLI. It is more stable than a versioned action that can disappear.

What I actually learned

Setting up CI doesn’t break things. It reveals what was already broken.

Every issue today was a pre-existing problem: wrong types, uncommitted changes, a lockfile out of sync, a root page that still said “Discover — coming soon” from an old version of the app. The app worked because local deploys were forgiving. CI is not forgiving. That’s the whole point.

The repo is the source of truth. If it’s not in git, it doesn’t exist.

Share

Discover more from shane.blog

Subscribe to get the latest posts sent to your email.