Broken builds that reach production are expensive. Oolvay ships with a production-ready CI workflow at .github/workflows/ci.yml that catches type errors, lint violations, and build failures on every push to main and master, and on every pull request regardless of source branch, before any code reaches your users.
The workflow focuses exclusively on validation. It performs no deployment and provisions no infrastructure. Type checking, linting, and a full Next.js production build run against every proposed change, with all required environment variables supplied as safe placeholder values so the build succeeds without real credentials.
The workflow cancels any in-progress run for the same branch when a new commit is pushed.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueThis prevents redundant builds queuing behind each other on fast-moving branches and conserves GitHub Actions minutes.
The workflow runs on ubuntu-latest with a 15-minute timeout. Every step runs in a single job named Type check, lint, build. There are no parallel jobs. Steps execute sequentially and the job fails immediately if any step exits with a non-zero code.
Install Bun via the official install script and add it to $GITHUB_PATH.
Restore three caches: the Bun dependency cache (~/.bun/install/cache),
node_modules, and the Next.js build cache (). The first two
are keyed on . The Next.js cache is keyed on plus any
changes under and , so it invalidates when source files
change even if dependencies have not.
0 / 2,000 characters
.next/cachebun.lockbun.lockapp/components/Install dependencies with bun install --frozen-lockfile. The lockfile flag
ensures CI never silently upgrades a package.
Run bunx fumadocs-mdx to generate Fumadocs source types. This must happen
before type checking, otherwise the TypeScript compiler will fail on
generated doc types.
Run bun run type-check, which executes tsc --noEmit in strict mode.
bun run lint via ESLint.Run bun run build to execute a full Next.js production build. Failure here
catches configuration errors, missing env vars, and broken imports that type
checking alone would not surface.
If any step fails, the workflow terminates immediately and marks the run as failed.
External services are neither contacted nor provisioned during CI. Every required environment variable is defined in the workflow with a placeholder value, allowing configuration validation, static analysis, and a full production build to succeed without real credentials. Three additional job-level variables are set regardless of the application: NODE_OPTIONS: "--no-warnings" suppresses Node.js warnings in output, FORCE_COLOR: true preserves colored output in logs, and CI: true signals to the build tooling that it is running in a CI environment. FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 is set at the workflow level to pin the Actions runner to Node 24.
The following categories are covered.
| Category | Variables |
|---|---|
| Application | NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_APP_VERSION |
| Authentication | BETTER_AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_BETTER_AUTH_URL |
| Database | DATABASE_URL |
RESEND_API_KEY, POSTMARK_API_KEY | |
| OAuth | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET |
| Infrastructure | UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, NEXT_PUBLIC_CLOUDFRONT_URL |
| Security | ARCJET_KEY, ARCJET_ENV, CRON_SECRET |
| Observability | SENTRY_ORG, SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_DSN, SENTRY_AUTH_TOKEN, NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST, AXIOM_TOKEN, AXIOM_DATASET, BETTERSTACK_SOURCE_TOKEN, BETTERSTACK_INGEST_URL |
| Payments | PAYMENT_PROVIDER, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_WEBHOOK_SECRET, RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECRET, NEXT_PUBLIC_RAZORPAY_KEY_ID |
| Notifications | ABLY_API_KEY, NEXT_PUBLIC_ABLY_CLIENT_KEY |
| Other | GOOGLE_PLACES_API_KEY |
Most variables are set to dummy since no external service is contacted during CI and the values are never exercised at runtime. The exception is variables that require a valid URL format to satisfy the Zod schema in env.ts, which are given well-formed dummy URLs instead: UPSTASH_REDIS_REST_URL, BETTERSTACK_INGEST_URL, NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_POSTHOG_HOST, and NEXT_PUBLIC_CLOUDFRONT_URL.
SENTRY_AUTH_TOKEN is the one variable whose dummy value changes build-time behaviour rather than just runtime behaviour. next.config.ts checks for it explicitly and skips the Sentry webpack plugin entirely when it is detected. See the Sentry in CI section below for the exact logic.
PAYMENT_PROVIDER is set to lemonsqueezy in CI. This is arbitrary. Any valid enum value satisfies the Zod schema. If you add a new required env var, add a corresponding placeholder here or the build step will fail.
Source map uploads and release creation are gated in next.config.ts on SENTRY_AUTH_TOKEN not being dummy.
const hasSentryToken =
!!process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_AUTH_TOKEN !== "dummy"
const sentryEnabled =
hasSentryToken && !!process.env.SENTRY_ORG && !!process.env.SENTRY_PROJECTIn CI, sentryEnabled evaluates to false and the build proceeds without the Sentry webpack plugin, keeping build times fast and avoiding failed upload attempts against a dummy token.
Common additions that fit naturally into the existing pipeline:
bun run test)bun auditAdd new steps before the build step if they gate the build, or as separate parallel jobs if they are independent.