EEPNext.js Template
Deployment

CI/CD

GitHub Actions quality gate, Trivy security scanning, and Renovate dependency management.

Overview

CI is defined in .github/workflows/ci.yml and runs on every push to every branch. Five jobs run in parallel, with e2e gated behind all of them:

lint ----------\
typecheck ------\
tests ----------+---> e2e
build ---------/
security ------/

If any of the first five fail, e2e is skipped. No point running a browser suite against broken or vulnerable code.


Jobs

JobCommandWhat it checks
Lintpnpm lintESLint - code style, import hygiene, TSDoc
Typecheckpnpm typecheckTypeScript - no type errors
Testspnpm test + pnpm test:tz + pnpm test:contractUnit, timezone, and contract tests
Buildpnpm buildNext.js production build succeeds
SecurityTrivy + GitleaksScans pnpm-lock.yaml for HIGH/CRITICAL CVEs and git history for leaked secrets
E2epnpm e2ePlaywright - auth flow and UI smoke tests

Secrets

The pipeline requires the following secrets, set in GitHub Settings -> Environments -> preview:

SecretRequired by
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYAll jobs (T3 Env validation)
CLERK_SECRET_KEYAll jobs (T3 Env validation)
DATABASE_URLBuild (T3 Env validation) + e2e (real reads/writes)
E2E_CLERK_USER_EMAIL_SUPER_ADMINE2e job only
E2E_CLERK_USER_EMAIL_ADMINE2e job only
E2E_CLERK_USER_EMAIL_NO_ROLEE2e job only

The Clerk redirect URL variables (NEXT_PUBLIC_CLERK_SIGN_IN_URL etc.) are not required - T3 Env defaults them to /sign-in, /sign-up, and /. NEXT_PUBLIC_DOCS_URL is optional and can be omitted in CI.

These secrets live in a GitHub environment named preview and are used only by CI and the test suite - never by the deployed app. GitHub's environment name is unrelated to Vercel's Preview/Production environments; it is simply the bucket CI pulls secrets from. The deployed app's env vars (including a real DATABASE_URL) are managed entirely in Vercel, per Vercel-environment, and CI never sees them. So you do not set a production DATABASE_URL here

  • only the throwaway test-branch URL described below.

Point DATABASE_URL at a dedicated, throwaway Neon test branch, never production. The roles.spec.ts e2e suite exercises the real write path (the SuperAdmin POST /api/example-items test does an INSERT), so every CI run leaves a junk row behind. Neon branching makes a persistent ci-test branch essentially free - create it once, migrate it (pnpm drizzle-kit migrate against its URL), and reuse it for every run. It never needs to be reset; the accumulating rows do no harm on a branch nothing else reads.


Security scanning

Two security tools run on every CI push, both in the security job.

Trivy

Trivy scans pnpm-lock.yaml for HIGH or CRITICAL CVEs and fails the build if any are found. This mirrors the pre-commit Trivy hook so vulnerabilities are caught in CI even if the hook was bypassed locally.

Trivy does not need to be installed on CI - the aquasecurity/trivy-action handles it. Locally, install with brew install trivy (macOS).

Trivy itself has faced supply chain attacks, so keep an eye on the GitHub Advisory Database for known vulnerabilities in your toolchain.

Gitleaks

Gitleaks scans the git history for accidentally committed secrets - API keys, tokens, passwords. It runs in CI by installing the CLI directly and locally as part of the pre-commit hook (staged files only, so it's fast).

Locally, install with brew install gitleaks (macOS). If gitleaks is not installed, the pre-commit hook warns but does not block the commit.

Handling false positives - add a path or pattern-based allowlist entry to .gitleaks.toml. This is committed to the repo so the allowlist is shared across the team. See the existing entries in .gitleaks.toml for the pattern to follow.

Avoid gitleaks-action for organisation repos. gitleaks-action@v3 requires a GITLEAKS_LICENSE secret for any GitHub organisation repo. Even after you have obtained a key environment-scoped secrets are not reliably passed to the action. This workflow installs the gitleaks CLI directly instead, which has no licence requirement:

- name: Install gitleaks
  run: curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz | tar -xz && sudo mv gitleaks /usr/local/bin/

- name: Run gitleaks secret scan
  run: gitleaks detect --no-banner

Dependency management

Do not use the Mend-hosted Renovate app for this repo. The obvious path - installing github.com/apps/renovate and letting Mend run it - fails here. Mend's runners have a hard 3GB memory cap, and our pnpm lockfile regeneration OOM-kills against it with a kernel-out-of-memory error.

Dependencies are managed by Renovate, configured in renovate.json at the repo root. Renovate is self-hosted here: it runs inside a GitHub Actions workflow (.github/workflows/renovate.yml) on GitHub's runners, not on Mend's hosted infrastructure.

Schedule

  • Monthly updates - the first day of each month, before 9am (Europe/London)
  • Security patches - immediately, on any schedule

The cadence is expressed in renovate.json as before 9am on the first day of the month. The workflow's own cron (0 6 1 * *) wakes Renovate at 06:00 UTC on the 1st; the renovate.json schedule gates when PRs actually open.

What it does

Renovate scans all dependencies across the root and docs/ workspace, groups related package families into single PRs, and opens them for review. It never auto-merges - all PRs require your approval before merging.

Package groups

Related packages are batched into single PRs so you're not flooded with individual bumps:

GroupPackages
ClerkAll @clerk/* packages
OpenTelemetryAll @opentelemetry/* + @vercel/otel
Radix UIAll @radix-ui/* packages
Testingvitest, Playwright, Testing Library
ESLinteslint, all plugins and stylistic packages
Drizzledrizzle-orm + drizzle-kit

Major version bumps always get their own PR and are labelled major-update - they may contain breaking changes and should be reviewed carefully.

Grouping is per-family, not one-PR-for-everything. Each group above collapses only its matching packages into a single PR. Anything that does not match a group rule (for example swr, postcss, @types/node) falls through to Renovate's default of one PR per package. So a run typically opens several PRs - the grouped families plus the ungrouped one-offs - not a single combined PR. This is deliberate: each PR stays coherent and independently mergeable, so a broken bump never blocks the safe ones. prConcurrentLimit (3) and prHourlyLimit (2) throttle how many open at once; the rest show as Rate-Limited on the Dependency Dashboard and are released as open PRs are merged or closed.

PR labels

LabelMeaning
dependenciesAll Renovate PRs
securityVulnerability patches
major-updateMajor version bumps

Self-hosting setup

Self-hosting means running Renovate yourself inside a GitHub Actions workflow instead of letting Mend's servers run it. The workflow (.github/workflows/renovate.yml) does exactly what Mend's bot did - reads renovate.json, opens grouped PRs - but on GitHub's ~7GB runners instead of Mend's 3GB-capped ones, which is what clears the OOM. Nothing about renovate.json changes: same grouping, same labels, same schedule.

The one-time setup below is done once per repo. It looks long but it's mostly clicking through a single web form - no code, no server, no webhooks.

The GitHub App you create below is a new, private App you own - it is NOT the public Mend-hosted "Renovate" app. Yours only mints a short-lived token so the workflow can open PRs under a bot identity. Do not follow GitHub's "writing code for a GitHub App" quickstart - that is for building a bot from scratch, which you don't need (Renovate is the bot).

Reference articles

Caveats

  • GITHUB_TOKEN alone is deliberately not used - Renovate's README states it's too restrictive to open PRs, so the App token is required.
  • App installation tokens expire after 1 hour; the workflow mints a fresh one per run via actions/create-github-app-token.
  • For a public repo, Actions minutes are free. If this repo is private, these monthly runs count against the org's Actions minutes (negligible at once a month).

The Mend dashboard

For this repo the Mend-hosted path OOMs (see the warning above), so you run Renovate from the Actions tab, not from Mend. This section is kept as a reference for the Mend dashboard itself - it applies to any repo small enough to run on Mend's hosted infrastructure, and to understand what the self-hosted workflow replaces.

When Renovate runs on Mend's hosted app, it comes with a dashboard at developer.mend.io - created for you when the Mend Renovate GitHub App is installed. Sign in with the same GitHub account you granted access to. It shows Renovate's status and lets you trigger a run on demand without waiting for the scheduled window.


E2e tests

Playwright runs against pnpm dev (not a production build). The dev server boots as part of the job - no separate deploy step needed.

On failure, the Playwright HTML report is uploaded as a GitHub Actions artifact (retained for 7 days). Download it from the Actions run summary and open it locally with pnpm e2e:report to inspect screenshots and traces.


Concurrency

Each branch has its own concurrency group. Pushing a new commit to a branch while CI is running cancels the in-progress run and starts a fresh one - stale runs don't queue up.


Branch protection

To require CI to pass before merging PRs to main, go to GitHub Settings -> Branches -> Add branch protection rule with branch pattern main:

  • Tick Require status checks to pass before merging
  • Add each check by name: Lint, Typecheck, Tests, Build, Security scan, E2e tests
  • Tick Require branches to be up to date before merging
  • Leave Require a pull request before merging unticked if you want to keep the ability to push directly to main

Status check names only appear in the search box after at least one CI run has completed. Push a branch and let CI run before setting up branch protection.


Coming soon

  • Neon preview database branching - isolated DB branch per PR, deleted on merge
  • Vercel preview deploy integration

On this page