Skip to main content

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 autorun command 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.