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
| Job | Command | What it checks |
|---|---|---|
| Lint | pnpm lint | ESLint - code style, import hygiene, TSDoc |
| Typecheck | pnpm typecheck | TypeScript - no type errors |
| Tests | pnpm test + pnpm test:tz + pnpm test:contract | Unit, timezone, and contract tests |
| Build | pnpm build | Next.js production build succeeds |
| Security | Trivy + Gitleaks | Scans pnpm-lock.yaml for HIGH/CRITICAL CVEs and git history for leaked secrets |
| E2e | pnpm e2e | Playwright - auth flow and UI smoke tests |
Secrets
The pipeline requires the following secrets, set in GitHub Settings -> Environments -> preview:
| Secret | Required by |
|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | All jobs (T3 Env validation) |
CLERK_SECRET_KEY | All jobs (T3 Env validation) |
DATABASE_URL | Build (T3 Env validation) + e2e (real reads/writes) |
E2E_CLERK_USER_EMAIL_SUPER_ADMIN | E2e job only |
E2E_CLERK_USER_EMAIL_ADMIN | E2e job only |
E2E_CLERK_USER_EMAIL_NO_ROLE | E2e 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-bannerDependency 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:
| Group | Packages |
|---|---|
| Clerk | All @clerk/* packages |
| OpenTelemetry | All @opentelemetry/* + @vercel/otel |
| Radix UI | All @radix-ui/* packages |
| Testing | vitest, Playwright, Testing Library |
| ESLint | eslint, all plugins and stylistic packages |
| Drizzle | drizzle-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
| Label | Meaning |
|---|---|
dependencies | All Renovate PRs |
security | Vulnerability patches |
major-update | Major 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
- Renovate GitHub Action - the action itself
- Running Renovate - self-host overview
- GitHub App permissions for Renovate
- Minting the App token in Actions
- Registering a GitHub App - the click-through form, no code
Caveats
GITHUB_TOKENalone 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