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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# 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'
# Manual + release-refresh dispatch (see website-release.yml, which fires this
# on a `v*` tag so /changelog reflects a new release). Defaults to production —
# a manual dispatch almost always means "deploy the site now"; pass
# environment=preview for a throwaway preview build.
workflow_dispatch:
inputs:
environment:
description: 'Deploy target'
type: choice
options:
default: production
# One deploy per ref; a newer push supersedes an in-flight one.
concurrency:
group: website-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
issues: read # the roadmap prebuild reads public issues/milestones via `gh`
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 == 'pull_request' && 'preview' || 'production' }})
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
# PRs → preview; a workflow_dispatch honors its `environment` input
# (default production); a push to main → production.
- name: Resolve deploy target
id: target
run: |
if [ "${{ github.event_name }}" = "pull_request" ] \
|| { [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ github.event.inputs.environment }}" = "preview" ]; }; then
echo "env=preview" >> "$GITHUB_OUTPUT"
echo "flag=" >> "$GITHUB_OUTPUT"
else
echo "env=production" >> "$GITHUB_OUTPUT"
echo "flag=--prod" >> "$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 + roadmap snapshot. NEXT_PUBLIC_POSTHOG_KEY is inlined into the client
# bundle at this step; GH_TOKEN lets the roadmap prebuild read issues/milestones
# via `gh` (degrades to a caveat-only page if absent).
- 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 }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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 }}