1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# Website deploy — build in GitHub Actions, deploy PREBUILT to Vercel (epic #196 / #198).
#
# Vercel serves; it does NOT build. `vercel build` runs `next build` on THIS GH runner
# (our minutes, not Vercel build minutes) and still compiles apps/website/vercel.json's
# `/relay` PostHog reverse-proxy rewrites into the deployed output. `vercel deploy
# --prebuilt` uploads that output without re-running the build on Vercel.
#
# vercel pull --environment=<preview|production> # fetch project settings/env
# vercel build [--prod] # next build here (GH runner)
# vercel deploy --prebuilt [--prod] # upload the prebuilt output
#
# push to main → production; pull_request → preview. The `prebuild` search-index +
# dead-nav-link check runs automatically as the `next build` lifecycle hook (a broken
# docs nav link fails the build). Path-filtered so CLI-only changes don't redeploy.
#
# Secrets (1Password → Private → "forgedb.dev deploy" → GH Actions via `make website-secrets`):
# VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID — link the CLI to the project.
# NEXT_PUBLIC_POSTHOG_KEY — publishable (phc_) key; injected at BUILD time so Next inlines
# it. The key lives in GH (not Vercel env) because the build runs here, not on Vercel (#192).
name: Website
on:
push:
branches:
paths:
- 'apps/website/**'
- '.github/workflows/website.yml'
pull_request:
paths:
- 'apps/website/**'
- '.github/workflows/website.yml'
workflow_dispatch:
# One deploy per ref; a newer push supersedes an in-flight one.
concurrency:
group: website-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write # to post the preview URL on PRs
# The Vercel CLI reads these to resolve the project without a committed .vercel/project.json.
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy:
name: build & deploy (${{ github.event_name == 'push' && 'production' || 'preview' }})
runs-on: ubuntu-22.04
defaults:
run:
working-directory: apps/website
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
# Cache ONLY bun's download cache (deps), keyed on the lockfile. We deliberately do
# NOT cache apps/website/.next/cache: restoring Next's persistent webpack cache across
# commits served STALE compiled CSS (a globals.css edit shipped byte-identical CSS and
# the fix never reached prod). The static-export build is fast, so recompiling CSS
# fresh every run is the right trade — correctness over a marginal cache hit.
# (`website-` keys from the old bun+Next cache are abandoned; `-bun-` starts clean.)
- name: Cache bun install
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: website-bun-${{ runner.os }}-${{ hashFiles('apps/website/bun.lock') }}
restore-keys: |
website-bun-${{ runner.os }}-
# Node/npm are preinstalled on the runner; install the Vercel CLI globally. `vercel
# build` still uses bun for the project itself (it detects apps/website/bun.lock).
- name: Install Vercel CLI
run: npm install -g vercel@latest
- name: Resolve deploy target
id: target
run: |
if [ "${{ github.event_name }}" = "push" ]; then
echo "env=production" >> "$GITHUB_OUTPUT"
echo "flag=--prod" >> "$GITHUB_OUTPUT"
else
echo "env=preview" >> "$GITHUB_OUTPUT"
echo "flag=" >> "$GITHUB_OUTPUT"
fi
- name: Pull Vercel project settings
run: vercel pull --yes --environment=${{ steps.target.outputs.env }} --token="$VERCEL_TOKEN"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
# `next build` runs here — including the `prebuild` search-index + dead-nav-link check.
# NEXT_PUBLIC_POSTHOG_KEY is inlined into the client bundle at this step.
- name: Build (on the GH runner)
run: vercel build ${{ steps.target.outputs.flag }} --token="$VERCEL_TOKEN"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
- name: Deploy prebuilt output
id: deploy
run: |
url=$(vercel deploy --prebuilt ${{ steps.target.outputs.flag }} --token="$VERCEL_TOKEN")
echo "url=$url" >> "$GITHUB_OUTPUT"
{
echo "### Website deployed (${{ steps.target.outputs.env }})"
echo ""
echo "$url"
} >> "$GITHUB_STEP_SUMMARY"
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
# Surface the preview URL on the PR so it's one click to review.
- name: Comment preview URL on PR
if: ${{ github.event_name == 'pull_request' }}
run: |
gh pr comment "$PR" --body "🚀 Website preview: ${{ steps.deploy.outputs.url }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}