Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ jobs:
if: failure()
run: cat /tmp/server.log || true

# Hard-gates on vulnerable direct/transitive deps. One advisory is
# ignored because it's upstream-blocked (uuid <14.0.0 via
# resend svix@1.90.0, dev-/server-side, no exploitable code path) —
# see CLAUDE.md "Audit advisories" for context and removal triggers.
# Any new advisory fails the job.
# Hard-gates on vulnerable direct/transitive deps. Two advisories are
# ignored because they're upstream-blocked (both via @lhci/cli@0.15.1
# and resend's transitive svix; both dev-/server-side with no
# exploitable code path) — see CLAUDE.md "Audit advisories" for
# context and removal triggers. Any new advisory fails the job.
- name: Dependency audit
run: bun audit --ignore=GHSA-w5hq-g745-h8pq
run: bun audit --ignore=GHSA-w5hq-g745-h8pq --ignore=GHSA-52f5-9888-hmc6

# Runs only on PRs (no baseline diff to compute on a push to main).
# Compares the PR's dependency manifest against main and flags
Expand Down
132 changes: 132 additions & 0 deletions .github/workflows/lighthouse.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Lighthouse

on:
pull_request:
workflow_dispatch:

permissions:
contents: read
pull-requests: write

# Including `github.workflow` in the key keeps this from colliding with any
# other workflow that happens to share a ref-based group prefix.
concurrency:
group: lighthouse-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lighthouse:
name: Lighthouse audit
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.13

# Bun is the package manager and script runner, but Next.js (and the
# lhci binary) run on Node. Pin Node via .nvmrc so a future GitHub
# bump can't break the audit silently.
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc

# Cache Bun's resolved package store keyed on the lockfile hash.
- name: Cache Bun install cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-

# Cache Next.js's incremental build output. Keyed on lockfile + commit
# SHA so the exact-key match is always per-commit fresh; restore-keys
# fall back to any previous build on the same lockfile so most CI runs
# hit a cache and skip rebuilding unchanged webpack modules.
- name: Cache Next.js build cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}-
${{ runner.os }}-nextjs-

- name: Install
run: bun install --frozen-lockfile

- name: Build
run: bun run build

# The action handles starting the server (per lighthouserc.cjs's
# `startServerCommand`), running Lighthouse against each URL, asserting
# the thresholds, and uploading reports to temporary public storage.
# Threshold rationale lives in lighthouserc.cjs alongside the values.
- name: Lighthouse CI
id: lighthouse
uses: treosh/lighthouse-ci-action@512cc908a55bfb0ad231facca52adf3d3a651df4 # v12
with:
configPath: ./lighthouserc.cjs
uploadArtifacts: true
temporaryPublicStorage: true

# Build a markdown table from the .lighthouseci/lhr-*.json reports.
# `unique_by(.url)` collapses duplicate rows if `numberOfRuns > 1`
# (the action writes the median LHR per URL, but be defensive);
# `sort_by(.url)` keeps row order deterministic across runs.
# `if: always()` so the PR comment still posts when assertions fail —
# reviewers can see *which* category regressed without digging through
# the workflow log.
#
# Per-cell emoji follows Lighthouse's own scoring buckets: 🟢 ≥90,
# 🟡 50–89, 🔴 <50. Floor thresholds in lighthouserc.cjs are below
# the 🟢 cutoff, so a cell can pass assertions and still show 🟡 — the
# visual signals "we locked this in but it's improvable."
- name: Format PR comment
if: always() && github.event_name == 'pull_request' && steps.lighthouse.outputs.links != ''
id: lh-comment
env:
LINKS_JSON: ${{ steps.lighthouse.outputs.links }}
run: |
{
echo 'body<<COMMENT_EOF'
echo '## 🔦 Lighthouse audit'
echo ''
echo '| URL | Perf | A11y | Best practices | SEO | Report |'
echo '|---|:---:|:---:|:---:|:---:|---|'
jq -s --argjson links "$LINKS_JSON" -r '
def emo(s): if s >= 90 then "🟢" elif s >= 50 then "🟡" else "🔴" end;
def cell(s): emo(s) + " " + (s | tostring);
map({
url: .finalUrl,
perf: (.categories.performance.score * 100 | floor),
a11y: (.categories.accessibility.score * 100 | floor),
bp: (.categories["best-practices"].score * 100 | floor),
seo: (.categories.seo.score * 100 | floor)
})
| unique_by(.url)
| sort_by(.url)
| map(
"| `" + (.url | sub("^https?://[^/]+"; "")) + "`"
+ " | " + cell(.perf)
+ " | " + cell(.a11y)
+ " | " + cell(.bp)
+ " | " + cell(.seo)
+ " | [report](" + ($links[.url] // "#") + ") |"
)
| join("\n")
' .lighthouseci/lhr-*.json
echo ''
echo "_Run [\`${GITHUB_RUN_ID}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) · $(date -u +'%Y-%m-%d %H:%M UTC')_"
echo 'COMMENT_EOF'
} >> "$GITHUB_OUTPUT"

- name: Post or update PR comment
if: github.event_name == 'pull_request' && steps.lh-comment.outputs.body != ''
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: lighthouse
message: ${{ steps.lh-comment.outputs.body }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ public/static

# claude code local settings (per-machine tool allowlists)
.claude

# lighthouse-ci output
.lighthouseci
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The site is optimized for performance with font optimization, analytics integrat

- **postcss `<8.5.10`** (GHSA-qx2v-qp2m-jg93, moderate XSS in CSS stringify). Multiple transitive resolutions — `next@16.2.4` pins `postcss@8.4.31` exactly, and `@tailwindcss/postcss@4.2.3` brings in `postcss@^8.5.6`. Resolved via `overrides.postcss = "8.5.10"` in `package.json`, which dedupes all transitives to the patched version. Drop the override after `next` and `@tailwindcss/postcss` ship releases that pull their transitives to ≥ 8.5.10.
- **mdast-util-to-hast `<13.2.1`** (GHSA-4fh9-h7wg-q85m, moderate XSS via unsanitized class attribute). Pulled in by three independent paths (shiki/rehype-pretty-code, react-markdown, velite/@mdx-js/mdx) — all parents accept `^13.0.0`, so the lockfile resolved to 13.2.0 (pre-fix). Resolved via `overrides.mdast-util-to-hast = "^13.2.1"`. Drop the override after parents ship releases that pull a patched version directly; verify with `bun pm ls --all | grep mdast-util-to-hast` showing only ≥ 13.2.1.
- **uuid `<14.0.0`** (GHSA-w5hq-g745-h8pq, moderate missing buffer bounds in v3/v5/v6 when `buf` provided). **Upstream-blocked.** Comes exclusively from `resend@6.12.2 → svix@1.90.0 → uuid@^10.0.0`. svix's declared range `^10.0.0` doesn't admit a 14.x override without risking the parent's CJS imports. Exposure is theoretical: `/api/subscribe` invokes Resend's send-email endpoint, which doesn't exercise svix's webhook-signing path, and the vulnerable code (v3/v5/v6 with explicit `buf`) isn't called. Remove the `--ignore` when `svix` (or `resend`) ships a release that bumps uuid to `^14.0.0`.
- **uuid `<14.0.0`** (GHSA-w5hq-g745-h8pq, moderate missing buffer bounds in v3/v5/v6 when `buf` provided). **Upstream-blocked.** Two parent paths: `resend@6.12.2 → svix@1.90.0 → uuid@^10.0.0` and `@lhci/cli@0.15.1 → uuid@8.3.2`. Neither parent admits a 14.x override without risking CJS imports. Exposure is theoretical on both: `/api/subscribe` uses Resend's send-email endpoint (not svix's webhook-signing path), `@lhci/cli` is dev-only and runs in CI on its own controlled inputs, and the vulnerable code (v3/v5/v6 with explicit `buf`) isn't called by either. Remove the `--ignore` when both parents ship releases bumping uuid to `^14.0.0`.
- **tmp `<=0.2.3`** (GHSA-52f5-9888-hmc6, low symbolic-link path traversal in `dir` param). **Upstream-blocked.** Pulled exclusively by `@lhci/cli@0.15.1` (dev-only, runs in CI on controlled inputs). The symlink-traversal scenario doesn't apply. Remove the `--ignore` when `@lhci/cli` ships a release with patched transitives.

CI hard-gates on `bun audit` (`.github/workflows/ci.yml`) with `--ignore=GHSA-w5hq-g745-h8pq` for the upstream-blocked uuid advisory. Any new advisory fails the job. The `dependency-review-action` PR job is a separate gate (license/severity-focused) that remains `continue-on-error: true` while a baseline of acceptable findings is established.
CI hard-gates on `bun audit` (`.github/workflows/ci.yml`) with `--ignore=GHSA-w5hq-g745-h8pq` and `--ignore=GHSA-52f5-9888-hmc6` for the upstream-blocked advisories. Any new advisory fails the job. The `dependency-review-action` PR job is a separate gate (license/severity-focused) that remains `continue-on-error: true` while a baseline of acceptable findings is established.
Loading
Loading