Skip to content

Commit e32fef5

Browse files
committed
Add GitHub Discussions-backed voting, truth predictions, and difficulty ratings
Replace the Cloudflare Worker + KV voting backend with GitHub Discussions. Likes are HEART reactions, truth predictions are THUMBS_UP/DOWN reactions, difficulty ratings are discussion comments. A shared App Engine proxy at formal-conjectures-web-worker.uc.r.appspot.com handles OAuth and anonymous reads using GitHub App installation tokens. Key changes: - voting.js: complete rewrite for GitHub Discussions backend - theorem.js/browse.js: enable voting integration and new sort options - appengine/: new App Engine proxy (replaces site/worker/) - build.js: accept pre-processed JSON from the live site - Workflow: -webtest branches skip Lean build, download JSON, inject config - Consent modal on first interaction explaining public nature of activity - OAuth callback routed through proxy so one URL works for all forks Interactive voting widgets are temporarily disabled; read-only discussion data (vote counts, predictions, difficulty) is displayed via the proxy.
1 parent a95325a commit e32fef5

22 files changed

Lines changed: 3603 additions & 2014 deletions

.github/workflows/build-and-docs.yml

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ on:
1818
push:
1919
branches:
2020
- main
21+
- '*-webtest'
2122
pull_request:
2223

2324
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
25+
# and creation of Discussions for conjectures (see site/create_discussions.js)
2426
permissions:
2527
contents: read
2628
pages: write
2729
id-token: write
30+
discussions: write
2831

2932
jobs:
3033
build:
@@ -37,14 +40,28 @@ jobs:
3740
with:
3841
fetch-depth: 0
3942

43+
- name: Detect webtest mode
44+
id: mode
45+
run: |
46+
if [[ "${{ github.ref_name }}" == *-webtest ]]; then
47+
echo "webtest=true" >> "$GITHUB_OUTPUT"
48+
echo "::notice::Webtest mode: skipping Lean build, downloading JSON from live site"
49+
else
50+
echo "webtest=false" >> "$GITHUB_OUTPUT"
51+
fi
52+
53+
# ---- Full Lean build (skipped on -webtest branches) ----
54+
4055
- name: Install elan
56+
if: steps.mode.outputs.webtest == 'false'
4157
run: |
4258
set -o pipefail
4359
curl -sSfL https://github.com/leanprover/elan/releases/download/v1.4.2/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz
4460
./elan-init -y --default-toolchain none
4561
echo "$HOME/.elan/bin" >> $GITHUB_PATH
4662
4763
- name: Generate All.lean
64+
if: steps.mode.outputs.webtest == 'false'
4865
run: |
4966
lake exe mk_all --lib FormalConjectures || true
5067
# Avoid including Test files from `Util/` to avoid inflating the
@@ -53,6 +70,7 @@ jobs:
5370
grep -v "FormalConjectures\.Util\." FormalConjectures.lean > FormalConjectures/All.lean
5471
5572
- name: Restore ~/.cache/mathlib
73+
if: steps.mode.outputs.webtest == 'false'
5674
uses: actions/cache/restore@v3
5775
with:
5876
path: ~/.cache/mathlib
@@ -62,15 +80,18 @@ jobs:
6280
oleans-
6381
6482
- name: Get olean cache
83+
if: steps.mode.outputs.webtest == 'false'
6584
run: |
6685
lake exe cache unpack
6786
lake exe cache get
6887
6988
- name: Build project
89+
if: steps.mode.outputs.webtest == 'false'
7090
run: |
7191
lake build
7292
7393
- name: Build literate source pages
94+
if: steps.mode.outputs.webtest == 'false'
7495
run: |
7596
cd docbuild
7697
# Some modules crash verso-literate (e.g. metaprogramming-heavy util files),
@@ -82,27 +103,32 @@ jobs:
82103
lake exe verso-html .lake/build/literate ../_literate_html || true
83104
84105
- name: Post-process literate HTML
106+
if: steps.mode.outputs.webtest == 'false'
85107
run: |
86108
python3 site/fix_literate_html.py _literate_html
87109
88110
- name: Pack olean cache
111+
if: steps.mode.outputs.webtest == 'false'
89112
run: |
90113
lake exe cache pack
91114
ls ~/.cache/mathlib
92115
93116
- name: Save ~/.cache/mathlib
117+
if: steps.mode.outputs.webtest == 'false'
94118
uses: actions/cache/save@v3
95119
with:
96120
path: ~/.cache
97121
key: oleans-${{ hashFiles('lake-manifest.json') }}-${{ hashFiles('**/*.lean') }}
98122

99123
- name: Prepare documentation
124+
if: steps.mode.outputs.webtest == 'false'
100125
run: |
101126
cd docbuild
102127
export MATHLIB_NO_CACHE_ON_UPDATE=1 # avoids "Failed to prune ProofWidgets cloud release: no such file or directory"
103128
lake update formal_conjectures
104129
105130
- name: Cache documentation
131+
if: steps.mode.outputs.webtest == 'false'
106132
uses: actions/cache@v5
107133
with:
108134
path: docbuild/.lake/build/doc
@@ -112,11 +138,13 @@ jobs:
112138
MathlibDoc-
113139
114140
- name: Build documentation
141+
if: steps.mode.outputs.webtest == 'false'
115142
run: |
116143
cd docbuild
117144
lake build FormalConjectures:docs
118145
119146
- name: Extract documentation
147+
if: steps.mode.outputs.webtest == 'false'
120148
run: |
121149
cd docbuild
122150
mkdir out
@@ -128,36 +156,71 @@ jobs:
128156
rsync -a --files-from=out-files.txt --relative .lake/build/doc ./out
129157
130158
- name: Set up Python
159+
if: steps.mode.outputs.webtest == 'false'
131160
uses: actions/setup-python@v6
132161
with:
133162
python-version: '3.12.9'
134163

135164
- name: Install Python dependencies
165+
if: steps.mode.outputs.webtest == 'false'
136166
run: |
137167
python -m pip install --upgrade pip
138168
pip install pandas==2.2.3 numpy==2.2.3 plotly==5.20.0 beautifulsoup4 lxml
139169
140170
- name: Run plotting script
171+
if: steps.mode.outputs.webtest == 'false'
141172
run: |
142173
python docbuild/scripts/plot_growth.py
143174
144175
- name: Inject stats into index.html
176+
if: steps.mode.outputs.webtest == 'false'
145177
shell: bash
146178
run: |
147179
cd docbuild
148180
lake exe overwrite_index ./out/index.html ./out/file_counts_dark.html ./out/file_counts_white.html | tee -a "$GITHUB_STEP_SUMMARY"
149181
182+
# ---- Webtest mode: download processed JSON from live site ----
183+
184+
- name: Download conjectures JSON from live site
185+
if: steps.mode.outputs.webtest == 'true'
186+
run: |
187+
mkdir -p site/data
188+
curl -sSfL https://google-deepmind.github.io/formal-conjectures/data/conjectures.json \
189+
-o site/data/conjectures.json
190+
echo "::notice::Downloaded $(wc -c < site/data/conjectures.json) bytes from live site"
191+
192+
# ---- Website build (always runs) ----
193+
150194
- name: Set up Node.js
151195
uses: actions/setup-node@v4
152196
with:
153-
node-version: '18'
197+
node-version: '22'
154198

155199
- name: Generate conjectures data for website
200+
if: steps.mode.outputs.webtest == 'false'
156201
run: |
157202
mkdir -p site/data
158-
lake exe extract_names --exclude=statement,docstring,formalProofKind,formalProofLink,moduleDocstrings > site/data/conjectures.json || true
203+
lake exe extract_names > site/data/conjectures.json || true
204+
205+
- name: Inject voting config
206+
run: |
207+
REPO_OWNER="${{ github.repository_owner }}"
208+
REPO_NAME="${{ github.event.repository.name }}"
209+
REPO_ID=$(curl -sSf -H "Authorization: Bearer ${{ github.token }}" \
210+
"https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}" | jq -r '.node_id')
211+
if [ -z "$REPO_ID" ] || [ "$REPO_ID" = "null" ]; then
212+
echo "::error::Failed to fetch repo node_id for ${REPO_OWNER}/${REPO_NAME}"
213+
exit 1
214+
fi
215+
echo "::notice::Configuring voting for ${REPO_OWNER}/${REPO_NAME} (${REPO_ID})"
216+
sed -i "s|REPLACE_WITH_PROXY_URL|https://formal-conjectures-web-worker.uc.r.appspot.com|" site/src/js/voting.js
217+
sed -i "s|REPLACE_WITH_CLIENT_ID|Iv23lid2mjCGp7EIKrJn|" site/src/js/voting.js
218+
sed -i "s|REPLACE_WITH_REPO_OWNER|${REPO_OWNER}|" site/src/js/voting.js
219+
sed -i "s|REPLACE_WITH_REPO_NAME|${REPO_NAME}|" site/src/js/voting.js
220+
sed -i "s|REPLACE_WITH_REPO_ID|${REPO_ID}|" site/src/js/voting.js
159221
160222
- name: Extract Verso fragments for website
223+
if: steps.mode.outputs.webtest == 'false'
161224
run: |
162225
python3 site/extract_verso_fragments.py _literate_html site/data/verso-fragments.json
163226
@@ -166,9 +229,19 @@ jobs:
166229
cd site
167230
node build.js
168231
env:
169-
BASE_PATH: /formal-conjectures
232+
BASE_PATH: /${{ github.event.repository.name }}
170233

171-
- name: Assemble deploy artifact
234+
- name: Create missing discussions
235+
if: github.ref == 'refs/heads/main' || endsWith(github.ref_name, '-webtest')
236+
run: |
237+
node site/create_discussions.js "${{ github.repository_owner }}" "${{ github.event.repository.name }}"
238+
env:
239+
GITHUB_TOKEN: ${{ github.token }}
240+
241+
# ---- Deploy artifact ----
242+
243+
- name: Assemble deploy artifact (full)
244+
if: steps.mode.outputs.webtest == 'false'
172245
run: |
173246
mkdir -p _deploy
174247
# Website goes at root
@@ -178,6 +251,12 @@ jobs:
178251
# Literate source pages go under /src
179252
cp -r _literate_html _deploy/src
180253
254+
- name: Assemble deploy artifact (webtest)
255+
if: steps.mode.outputs.webtest == 'true'
256+
run: |
257+
mkdir -p _deploy
258+
cp -r site/site/* _deploy/
259+
181260
- name: Upload deploy artifact
182261
id: deployment
183262
uses: actions/upload-pages-artifact@v4
@@ -186,7 +265,7 @@ jobs:
186265

187266
# Deployment job
188267
deploy:
189-
if: github.ref == 'refs/heads/main'
268+
if: github.ref == 'refs/heads/main' || endsWith(github.ref_name, '-webtest')
190269
environment:
191270
name: github-pages
192271
url: ${{ steps.deployment.outputs.page_url }}
@@ -196,3 +275,6 @@ jobs:
196275
- name: Deploy to GitHub Pages
197276
id: deployment
198277
uses: actions/deploy-pages@v4
278+
279+
- name: Print site URL
280+
run: echo "::notice::Site deployed to ${{ steps.deployment.outputs.page_url }}"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
.cache
55
# `lake` stores its files here:
66
.lake
7+
# App Engine secrets (never commit real credentials)
8+
site/appengine/app.secrets.yaml
79

810
# Verso literate build output (generated by `lake build :literate` + `verso-html`)
911
_literate_html/

site/appengine/.gcloudignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

site/appengine/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# App Engine Proxy
2+
3+
Handles GitHub App OAuth token exchange and anonymous discussion reads for the voting system. One deployment serves all forks.
4+
5+
For an overview of the voting system, see [`docs/voting.md`](../docs/voting.md).
6+
7+
## Shared Proxy
8+
9+
The default deployment at `formal-conjectures-web-worker.uc.r.appspot.com` is used automatically by the CI workflow. Forkers don't need their own proxy — just install the [formal-conjectures-voting](https://github.com/apps/formal-conjectures-voting) GitHub App on their fork.
10+
11+
The shared proxy:
12+
- Uses GitHub App installation tokens (works for any repo where the app is installed)
13+
- Only serves `google-deepmind/formal-conjectures` and its forks
14+
- Allows any `*.github.io` origin via CORS
15+
- Routes OAuth callbacks so a single registered callback URL works for all forks
16+
17+
## Running Your Own Proxy
18+
19+
### Prerequisites
20+
21+
- [Node.js](https://nodejs.org/) >= 22
22+
- [Google Cloud SDK](https://cloud.google.com/sdk/docs/install)
23+
- A GCP project with App Engine enabled
24+
25+
### Create a GitHub App
26+
27+
1. Go to https://github.com/settings/apps/new
28+
2. Set **Callback URL** to `https://YOUR_PROJECT.REGION.r.appspot.com/oauth/callback`
29+
3. Uncheck **Webhook > Active**
30+
4. Under **Repository permissions**, set **Discussions** to **Read & write**
31+
5. Click **Create GitHub App**
32+
6. Note the **App ID**, copy the **Client ID**
33+
7. Generate a **client secret** and a **private key** (PEM file)
34+
8. Install the app on your target repo(s)
35+
36+
### Store Secrets
37+
38+
```bash
39+
gcloud config set project YOUR_PROJECT
40+
gcloud services enable secretmanager.googleapis.com
41+
42+
echo -n "your_client_id" | gcloud secrets create GH_CLIENT_ID --data-file=-
43+
echo -n "your_client_secret" | gcloud secrets create GH_CLIENT_SECRET --data-file=-
44+
gcloud secrets create GH_APP_PRIVATE_KEY --data-file=path/to/private-key.pem
45+
46+
PROJECT_ID=$(gcloud config get-value project)
47+
for SECRET in GH_CLIENT_ID GH_CLIENT_SECRET GH_APP_PRIVATE_KEY; do
48+
gcloud secrets add-iam-policy-binding $SECRET \
49+
--member="serviceAccount:${PROJECT_ID}@appspot.gserviceaccount.com" \
50+
--role="roles/secretmanager.secretAccessor"
51+
done
52+
```
53+
54+
### Deploy
55+
56+
Edit `app.yaml` to set `GH_APP_ID` to your App ID, then:
57+
58+
```bash
59+
cd site/appengine
60+
npm install
61+
gcloud app deploy
62+
```
63+
64+
Update `WORKER_URL` in `voting.js` to your App Engine URL.
65+
66+
### Local Development
67+
68+
```bash
69+
cd site/appengine && npm install
70+
export GH_APP_ID=your_app_id
71+
export GH_CLIENT_ID=your_client_id
72+
export GH_CLIENT_SECRET=your_client_secret
73+
export GH_APP_PRIVATE_KEY="$(cat path/to/private-key.pem)"
74+
export ALLOWED_ORIGIN=http://localhost:8000
75+
node server.js
76+
```
77+
78+
Proxy runs on `http://localhost:8080`. Secrets from env vars skip Secret Manager.
79+
80+
## API Endpoints
81+
82+
### `POST /token`
83+
84+
Exchanges a GitHub OAuth `code` for an access token. Body: `{ "code": "..." }`.
85+
86+
### `GET /oauth/callback?return_to=URL`
87+
88+
OAuth redirect bounce. Validates `return_to` is `*.github.io` or localhost, appends the `code` parameter, and redirects.
89+
90+
### `GET /discussions?owner=OWNER&repo=REPO`
91+
92+
Aggregated discussion data (votes, predictions, difficulty). Uses GitHub App installation tokens. Cached 60 seconds. Returns 403 if the repo is not `google-deepmind/formal-conjectures` or a fork of it.
93+
94+
## Configuration
95+
96+
| Variable | Source | Description |
97+
|---|---|---|
98+
| `ALLOWED_ORIGIN` | `app.yaml` / env | Extra CORS origins (`.github.io` allowed automatically) |
99+
| `GH_APP_ID` | `app.yaml` / env | GitHub App ID |
100+
| `GH_CLIENT_ID` | Secret Manager / env | GitHub App client ID |
101+
| `GH_CLIENT_SECRET` | Secret Manager / env | GitHub App client secret |
102+
| `GH_APP_PRIVATE_KEY` | Secret Manager / env | GitHub App private key (PEM) |
103+
104+
## Cost
105+
106+
App Engine F1 instances include 28 free instance-hours/day. With `max_instances: 1`, this stays within the free tier.

site/appengine/app.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
runtime: nodejs22
2+
3+
instance_class: F1
4+
5+
automatic_scaling:
6+
max_instances: 1
7+
8+
env_variables:
9+
GH_APP_ID: "3005907"
10+
# CORS: any *.github.io origin is allowed automatically.
11+
# ALLOWED_ORIGIN is only needed for additional origins (e.g. custom domains).
12+
ALLOWED_ORIGIN: ""
13+
# Secrets (GH_CLIENT_ID, GH_CLIENT_SECRET, GH_APP_PRIVATE_KEY) are loaded
14+
# from Google Cloud Secret Manager at startup. See README.md for setup.

0 commit comments

Comments
 (0)