Small teams often treat CI as something you set up once and ignore. The result is a pipeline that passes even when the build is broken, or a suite so slow and flaky that developers disable it and push direct to main. Neither outcome serves anyone. This article covers the four gates we use on ManOps - our internal operations product - including why each one is in the pipeline, the order they run in, and the post-deploy smoke check we added after a dependency upgrade silently broke production.
Why Gate Order Matters
The goal of a CI pipeline is to fail as fast as possible on the cheapest signal. Type checking takes seconds. Running a full test suite takes minutes. Building the production bundle takes longer. Putting these in the wrong order means waiting three minutes for a type error you could have caught in ten seconds.
Our CI job for the ManOps web application runs these steps in order: typecheck, lint, test, build. Every step must pass before the next runs. Pull requests and pushes to main both run the full sequence. Here is what each step catches and why it earns its place.
Gate 1: TypeScript Strict Check
TypeScript type checking runs first because it is the fastest gate and it catches a category of bug that nothing else does. We run TypeScript in strict mode, which means the compiler checks for implicit any, non-null assertion abuse, and type narrowing gaps that lenient configurations let through.
The practical value of running typecheck in CI - rather than relying on developer editors - is that not every contributor has the same editor setup, and some changes introduced in one file break type inference in another file that was never touched. CI catches these cross-file breakages on every PR, not just when the author happens to open the affected file.
If typecheck fails, the pipeline stops immediately. There is no point running lint, tests, or a build on code that does not type-check - the results would be meaningless.
Gate 2: Lint
Lint runs second. In the ManOps pipeline we run `next lint` with ESLint and the Next.js core-web-vitals config. It is a deliberately small ruleset, and any lint error fails the CI job.
The reason lint sits between typecheck and tests rather than before typecheck is practical: typecheck catches more critical issues faster. Lint is stylistic enforcement plus a smaller class of semantic errors. Running it after typecheck means you only lint code that at least compiles correctly.
One rule worth emphasising: keep the lint step fast and focused. A slow lint step is the first thing developers disable when they need to push quickly.
Gate 3: Tests
The ManOps web test suite had 2,074 tests when the incident described below happened, and the Flutter apps have their own unit and widget suites. For the web app CI run, we use Vitest. Tests are the most expensive gate in time, which is why they run after the faster checks. A build that fails typecheck or lint never reaches the test run.
What our test suite covers: unit tests for individual functions, API-route tests, and tests of the data repository layer. We do not run end-to-end browser tests in CI today. ManOps relies on unit, API-route and component tests, plus a post-deploy smoke check against the live API. Putting E2E tests in the main PR gate would make the pipeline too slow to be useful, and we have not built that layer yet.
The principle that guided how we built the suite: test at the level closest to the thing you want to verify. Testing a form's validation logic as a unit test is faster and more targeted than testing it through a full browser flow. The browser test exists to verify that the pieces work together, not to re-verify the logic already covered by unit tests.
Gate 4: Production Build
The build step runs last. For a Next.js application, the production build catches a class of error that does not appear in development mode: missing environment variables that are required at build time, incompatible module imports that the development server polyfills, and prerendering failures on pages that require runtime data.
Running the build in CI with placeholder environment variables (not real credentials, just non-empty strings that satisfy the type checks and allow prerendering to proceed without real data) means you learn about build-time errors before they reach the deployment step. A pull request that passes typecheck, lint, and tests but breaks the production build still fails the CI gate.
The Native App CI: a Separate Workflow
The ManOps Flutter apps run a separate GitHub Actions workflow on pull requests that touch the native directory. On a Linux runner it checks formatting and generated files, runs flutter analyze, the unit and widget test suite, and an Android debug build; a separate job builds and tests the Windows app. iOS and macOS builds are not in the automatic CI run - on GitHub's published per-minute rates a macOS runner costs about ten times a standard Linux runner ($0.062 against $0.006), and we run iOS builds locally on a Mac before a PR is opened. The macOS CI job exists but requires manual dispatch from the GitHub Actions UI.
This is a deliberate trade-off: we accepted the small risk of an iOS-specific build issue reaching a PR in exchange for keeping the native CI affordable. The trade-off is worth revisiting if iOS-specific bugs become a pattern.
The Post-Deploy Smoke Check: Why It Exists
On 3 October 2026, a dependency upgrade on ManOps passed every CI gate - typecheck passed, lint passed, all 2,074 tests passed, the build succeeded. The deployment to Vercel completed. Then every API route in production crashed.
The root cause was an ESM compatibility issue: firebase-admin 14 depends on jwks-rsa, which require()s the ESM-only jose, and that triggered an ERR_REQUIRE_ESM error at runtime on Vercel's Node.js runtime. This error only appeared when the production bundle actually ran - nothing in the build-time or test-time environment caught it, because tests run in a Node environment where the import resolution behaved differently.
The application was broken for approximately one hour before the upgrade was reverted. We added a post-deploy smoke check specifically because of this incident.
The smoke check is a GitHub Actions workflow triggered by the deployment_status event, which Vercel's GitHub integration sends. When a production deployment succeeds, the workflow pings two endpoints: /api/meta (a public endpoint that loads the same server runtime as all other routes) and /login (a public page). If either returns anything other than 200 after six attempts with ten-second waits, the workflow fails and GitHub sends a notification. The job has a five-minute timeout, and it would have flagged the 3 October deployment as broken instead of leaving us to find out from errors. One caveat: Vercel's docs now encourage migrating from deployment_status to repository_dispatch events, so check which one your project should use.
What the Smoke Check Does Not Replace
A smoke check is not a functional test suite. It confirms the application is running and the server runtime is not crashing. It does not verify that user journeys work, that data is returning correctly, or that a specific feature is functional. Those are the job of the test suite run before deployment. The smoke check is a last-line check that confirms the deployed version is alive.
The practical configuration decision is which endpoints to probe. We chose /api/meta because it is public, lightweight, returns a known JSON shape that we validate by checking for a specific key, and loads the same server runtime as every authenticated API route. A 500 on that endpoint almost always means the server runtime is broken.
The CI Gates That Small Teams Skip and Should Not
- TypeScript strict mode in CI. Many teams run TypeScript but do not enforce strict mode, or run typecheck locally but not in the pipeline. The value is precisely in catching the errors that editors and dev-mode builds paper over.
- Treating lint errors as failures. Running lint in 'warn' mode means lint finds nothing - warnings accumulate and developers learn to ignore them. Treat lint errors as hard failures or do not run lint at all.
- Post-deploy smoke checks. The build passing is not the same as the deployment working. ERR_REQUIRE_ESM, missing environment variables at runtime, and misconfigured headers are all things that pass a build and fail a live deployment.
- Keeping CI fast enough to trust. A pipeline that takes 20 minutes gets skipped or worked around. Keep the main PR gate under ten minutes. Move slow checks to scheduled or manually-triggered workflows.
The CI/CD pipelines overview covers the broader topic if you are deciding what kind of pipeline is appropriate for your stage. For the ManOps setup in more detail, the ManOps project page covers the architecture and technical context.
If your current CI is ignored, broken, or does not exist, and you want to understand what a useful baseline looks like for your stack, ask about support and hosting. We can review your current setup and identify the gaps that matter most.
Common questions
What order should CI checks run in?
Run the cheapest, fastest signal first so the pipeline fails fast. The ManOps pipeline runs typecheck, then lint, then tests, then the production build, and each step must pass before the next one starts.
Why did my deploy break production when every CI check passed?
A passing build is not the same as a working deployment. On 3 October 2026 a ManOps dependency upgrade passed typecheck, lint, 2,074 tests and the build, then crashed every API route in production. A post-deploy smoke check on live endpoints catches this.
When should I get someone to review my CI setup?
If your current CI is ignored, broken or does not exist, a review can show what a useful baseline looks like for your stack. Zovintra can review your setup and identify the gaps that matter most.
Sources
- Events that trigger workflows (deployment_status) - GitHub Docs, accessed 19 Oct 2026
- Actions runner pricing - GitHub Docs, accessed 19 Oct 2026
- Deploying GitHub Projects with Vercel - Vercel Docs, accessed 19 Oct 2026



