-
-
Notifications
You must be signed in to change notification settings - Fork 629
225 lines (207 loc) · 9.87 KB
/
Copy pathpr-preview.yml
File metadata and controls
225 lines (207 loc) · 9.87 KB
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
name: PR preview
# Stage 1 of 2: BUILD ONLY. No secrets are available to this workflow.
#
# This job runs `npm ci` and a full Vite build from the pull request's own
# tree, so on a fork PR it executes code written by an outside contributor.
# That is why it holds nothing worth stealing: `permissions` is read-only and
# no `secrets.*` is referenced anywhere below. It produces a `pr-preview-site`
# artifact and stops.
#
# Stage 2 (.github/workflows/pr-preview-deploy.yml) picks that artifact up on
# `workflow_run` and publishes it. That job DOES hold PREVIEW_DEPLOY_TOKEN and
# CLOUDFLARE_API_TOKEN, but it only moves static files -- it never runs
# contributor code. Splitting it this way is what lets previews work for fork
# PRs at all: the `pull_request` event withholds repository secrets from forks,
# and `workflow_run` runs from the default branch with them restored.
#
# Do NOT "fix" anything here by switching to pull_request_target: that would
# put the secrets back into the same job as the contributor's build script,
# which is the exact arrangement this split exists to avoid.
#
# The build-time `secrets.*` still referenced below are the exception that
# proves the rule. On a fork-triggered `pull_request` run GitHub resolves every
# one of them to an empty string, so a fork build never sees them; on a
# maintainer's same-repo branch they resolve normally, exactly as before. The
# practical effect is that fork previews build without the Google Maps /
# Protomaps / GEE keys and those basemaps are absent from the preview, while
# maintainer previews are unchanged. That asymmetry is the point -- do not
# "fix" it by moving the keys somewhere a fork run can read.
#
# Do NOT add any secret here whose absence a fork build cannot tolerate, and
# never the deploy tokens: those live in stage 2 only.
#
# Publishing fork-authored HTML/JS under opengeos.org/pages-preview/ puts it on
# a real origin the project owns. That is inherent to previewing a fork, but it
# means the repository's "Require approval for all external contributors"
# setting is load-bearing: stage 1 will not run until a maintainer approves the
# run, and stage 2 only fires after stage 1 succeeds, so that approval gates
# the publish too. Keep it on.
#
# The build mirrors .github/workflows/pages.yml (the GitHub Pages production
# deploy) so previews match what ships:
# docs deps -> jupyterlite deps -> npm ci -> web demo build -> docs build ->
# bundle demo into site/demo.
#
# Optional build-time values, baked into the client bundle, same as pages.yml:
# VITE_GEE_OAUTH_CLIENT_ID, VITE_PROTOMAPS_API_KEY,
# VITE_GOOGLE_MAPS_API_KEY or GOOGLE_MAPS_API_KEY, VITE_MAPILLARY_ACCESS_TOKEN
# Empty on fork runs, per the note above.
#
# The deploy secrets that used to live here (PREVIEW_DEPLOY_TOKEN,
# CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) moved to stage 2.
on:
pull_request:
branches: [main]
types: [opened, reopened, synchronize, closed]
workflow_dispatch:
# Read-only, and deliberately so -- see the header. The PR comment is posted by
# stage 2, which has `pull-requests: write`.
permissions:
contents: read
concurrency:
group: pr-preview-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build preview
runs-on: ubuntu-latest
steps:
# On `closed` there is nothing to build, but the job still has to run and
# succeed: it is what triggers stage 2, which removes the published
# preview. Every build step below is skipped and no artifact is uploaded;
# stage 2 reads the PR's state from the API rather than from anything
# this job hands it.
- name: Note the removal trigger
if: github.event.action == 'closed'
run: echo "PR closed -- stage 2 will remove the preview."
- name: Checkout repository
if: github.event.action != 'closed'
uses: actions/checkout@v7
with:
# Match ci.yml: don't leave the GITHUB_TOKEN in .git/config where an
# npm lifecycle script during `npm ci`/`npm run build` could read it.
persist-credentials: false
- name: Set up Python
if: github.event.action != 'closed'
uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: |
requirements-docs.txt
apps/geolibre-desktop/jupyterlite/requirements.txt
- name: Install documentation dependencies
if: github.event.action != 'closed'
run: python -m pip install -r requirements-docs.txt
- name: Install JupyterLite build dependencies
if: github.event.action != 'closed'
run: python -m pip install -r apps/geolibre-desktop/jupyterlite/requirements.txt
- name: Set up Node.js
if: github.event.action != 'closed'
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: npm
cache-dependency-path: package-lock.json
- name: Install frontend dependencies
if: github.event.action != 'closed'
run: npm ci
- name: Build web demo
if: github.event.action != 'closed'
run: npm run build -w geolibre-desktop
env:
GEOLIBRE_APP_BASE: ./
# All empty on a fork run -- see the header. Mapillary additionally
# falls back to a public repository variable, so that one basemap
# does survive into fork previews.
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
VITE_PROTOMAPS_API_KEY: ${{ secrets.VITE_PROTOMAPS_API_KEY }}
VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
VITE_MAPILLARY_ACCESS_TOKEN: ${{ secrets.VITE_MAPILLARY_ACCESS_TOKEN || vars.VITE_MAPILLARY_ACCESS_TOKEN }}
VITE_GEOLIBRE_COLLAB_URL: wss://collab.geolibre.app
- name: Build documentation
if: github.event.action != 'closed'
run: zensical build --strict
- name: Add web demo to site
if: github.event.action != 'closed'
run: |
mkdir -p site/demo
cp -R apps/geolibre-desktop/dist/. site/demo/
test -f site/demo/index.html
test -f site/demo/jupyterlite/lab/index.html
- name: Check the site fits GitHub Pages
if: github.event.action != 'closed'
run: |
set -euo pipefail
echo "preview size: $(du -sh site | cut -f1)"
# 104857600 bytes = 100 MB, the Pages per-file hard limit.
oversized=$(find site -type f -size +104857600c -printf '%s\t%p\n' || true)
if [ -n "$oversized" ]; then
echo "::error::Files exceed the 100 MB GitHub Pages limit:"
echo "$oversized"
exit 1
fi
- name: Map oversized DuckDB WASM to a CDN redirect
if: github.event.action != 'closed'
# Cloudflare Pages rejects any single file > 25 MiB. The bundled DuckDB
# WASM modules (duckdb-eh ~35 MiB, duckdb-mvp ~40 MiB) exceed that, so
# we point their hashed URLs at jsDelivr at the exact pinned version
# (byte-identical to the bundled file).
#
# This step only WRITES site/_redirects; it does not delete anything.
# GitHub Pages allows 100 MB per file and ignores `_redirects`, so the
# Pages copy keeps serving the modules locally. Stage 2 deletes them
# right before the Cloudflare deploy, once Pages has had the full set.
#
# Any OTHER oversized file is a hard error here, which is also what
# lets stage 2 delete by size alone: if this step passed, every file
# over 25 MiB has a redirect.
run: |
set -euo pipefail
version=$(node -e "console.log(require('./node_modules/@duckdb/duckdb-wasm/package.json').version)")
echo "duckdb-wasm version: $version"
redirects=site/_redirects
touch "$redirects"
mapped=0
# 26214400 bytes = 25 MiB, Cloudflare Pages' exact per-file limit.
while IFS= read -r -d '' f; do
rel=${f#site}
base=$(basename "$f")
case "$base" in
duckdb-eh-*) cdn=duckdb-eh.wasm ;;
duckdb-mvp-*) cdn=duckdb-mvp.wasm ;;
duckdb-coi-*) cdn=duckdb-coi.wasm ;;
*)
echo "::error::Oversized file not handled by CDN redirect: $rel ($(du -h "$f" | cut -f1)). Cloudflare Pages rejects files > 25 MiB."
exit 1 ;;
esac
echo "$rel https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@${version}/dist/${cdn} 302" >> "$redirects"
echo "redirect: $rel -> $cdn"
mapped=$((mapped + 1))
done < <(find site -type f -size +26214400c -print0)
echo "mapped $mapped oversized file(s)"
echo "----- site/_redirects -----"
cat "$redirects"
# The Pages publish in stage 2 happens via `git add`, which honors any
# .gitignore inside the payload and would silently drop those files from
# the commit while the build still reports success.
- name: Strip .gitignore files from the site
if: github.event.action != 'closed'
run: find site -name .gitignore -print -delete
- name: Upload the built site
if: github.event.action != 'closed'
uses: actions/upload-artifact@v7
with:
name: pr-preview-site
path: site
# upload-artifact drops dotfiles by default, which would quietly gut
# parts of the JupyterLite build. The .gitignore strip above already
# ran, so nothing unwanted comes back with them.
include-hidden-files: true
if-no-files-found: error
# The payload is a few hundred MB and is consumed minutes later.
retention-days: 1
# Mostly pre-compressed wasm/js; level 6 costs minutes for almost no
# size win on this payload.
compression-level: 1