Usage Examples
Practical, copy-pasteable examples for the pieces described in Getting Started: registering a repo with the CLI, auditing a page that requires login, and wiring the CI upload step.
Register a repo with the CLI
lhci-toolkit (from @houses/lighthouse-cli) registers a repo as a project
on your LHCI server and prints the build token you'll use in CI:
lhci-toolkit --server-url=https://your-lhci-server.onrender.com \
--admin-username=<username> --admin-password=<password> \
register <repo-name>
This prints:
- The build token to use in CI (
--upload.token) - A one-time admin token — save it somewhere retrievable; it's the only way to rename or delete the project later, and it's shown exactly once
- A ready-to-copy
lhci autoruncommand for wiring into your CI pipeline
Pass --base-branch <branch> (defaults to main) if your repo's diff base
isn't main. --admin-username/--admin-password are required whenever the
server has basicAuth enabled — it gates registration the same way it gates
the build-upload endpoint.
Audit a page that requires login
@houses/lighthouse-login-helpers ships three helpers for different auth
gates. For a plain username/password login form (local/dev/staging, no
CAPTCHA), loginWithCredentials drives the form directly:
import type { Page } from 'puppeteer';
import { loginWithCredentials } from '@houses/lighthouse-login-helpers';
export default async function login(page: Page): Promise<void> {
await loginWithCredentials(page, {
loginUrl: 'https://staging.example.com/login',
usernameSelector: '#email',
passwordSelector: '#password',
submitSelector: 'button[type="submit"]',
username: process.env.LOGIN_USERNAME!,
password: process.env.LOGIN_PASSWORD!,
successSelector: '[data-testid="dashboard"]',
timeoutMs: 15000, // optional, defaults to 15000
});
}
Wire that script into lighthouserc.js via collect.puppeteerScript, so
Lighthouse CI runs the login flow before auditing the authenticated page:
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/dashboard'],
startServerCommand: 'npm run start',
puppeteerScript: './login.js',
},
upload: {
target: 'lhci',
serverBaseUrl: 'https://your-lhci-server.onrender.com',
token: process.env.LHCI_BUILD_TOKEN,
basicAuth: {
username: process.env.LHCI_BASIC_AUTH_USERNAME,
password: process.env.LHCI_BASIC_AUTH_PASSWORD,
},
},
},
};
If the target app is gated by CAPTCHA/Turnstile instead — where scripting
the login form isn't reliable, and may violate the CAPTCHA provider's terms —
use injectSessionCookies with a pre-captured session instead. For plain
HTTP basic auth, use basicAuthHeader or authenticateBasicAuth. See the
package README for all three signatures.
Wire the GitHub Actions upload step
Once a repo is registered, add a workflow step that builds the app and
uploads the Lighthouse report. It needs three secrets: a project-specific
build token from register, plus the server's shared basicAuth pair (the
same gate that protects the dashboard also protects the upload endpoint):
name: Lighthouse CI
on:
pull_request:
push:
branches: [main]
jobs:
lhci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx @lhci/cli autorun
env:
LHCI_BUILD_TOKEN: ${{ secrets.LHCI_BUILD_TOKEN }}
LHCI_BASIC_AUTH_USERNAME: ${{ secrets.LHCI_BASIC_AUTH_USERNAME }}
LHCI_BASIC_AUTH_PASSWORD: ${{ secrets.LHCI_BASIC_AUTH_PASSWORD }}
# Enables LHCI's automatic PR status check (score + comparison
# link posted on the PR itself) instead of only a log link.
# This is the workflow's own ambient token, not a new secret.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Own build vs. live URL: two separate projects
If a repo also needs (or can only do) a live-URL audit — auditing the
already-deployed site instead of building it in CI — register a second
project named <repo>-live rather than sharing the one project between both
workflows.
The LHCI server dedupes builds per (project, commit hash). If an own-build
workflow and a live-audit workflow both upload for the same commit into the
same project, whichever upload lands second gets rejected with a 422.
Splitting into two projects (<repo> for the own-build path,
<repo>-live for the live-audit path) gives each workflow its own token and
its own build history, so they never collide.
This split also solves a separate, common problem: a repo whose own build
needs backend/CMS secrets that aren't available in CI (for example, static
generation that hits a live database) can skip building entirely for the
live-audit path. That workflow collects against the real deployed URL over
the network instead of using startServerCommand, so it never needs those
secrets in the first place.