name: Cloudflare preview # Builds the docs site + web demo for every pull request and uploads it to a # Cloudflare Pages project as a *preview* deployment, then comments the URL on # the PR. This replaces the old Netlify Deploy Previews. # # 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. # # NOTE on fork PRs: the `pull_request` event does NOT expose repository secrets # to PRs opened from forks, so previews only run for branches pushed to this # repo (maintainers and collaborators). External fork PRs are skipped — the job # guard below short-circuits them instead of failing with a missing token. # # Required repository secrets (already present for the Workers deploys): # CLOUDFLARE_API_TOKEN - needs the "Cloudflare Pages: Edit" permission # CLOUDFLARE_ACCOUNT_ID # Optional (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 on: pull_request: branches: [main] workflow_dispatch: permissions: contents: read pull-requests: write concurrency: group: cf-preview-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: preview: name: Build and deploy preview runs-on: ubuntu-latest # Skip fork PRs: secrets are unavailable there, so the deploy would fail. if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository steps: - name: Checkout repository uses: actions/checkout@v6 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 uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip cache-dependency-path: | requirements-docs.txt apps/geolibre-desktop/jupyterlite/requirements.txt - name: Install documentation dependencies run: python -m pip install -r requirements-docs.txt - name: Install JupyterLite build dependencies run: python -m pip install -r apps/geolibre-desktop/jupyterlite/requirements.txt - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: lts/* cache: npm cache-dependency-path: package-lock.json - name: Install frontend dependencies run: npm ci - name: Build web demo run: npm run build -w geolibre-desktop env: GEOLIBRE_APP_BASE: ./ 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 run: zensical build --strict - name: Add web demo to site 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: Offload oversized DuckDB WASM to CDN # 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 # drop the local copies and redirect their hashed URLs to jsDelivr at the # exact pinned version (byte-identical to the bundled file). This only # affects the Cloudflare preview; GitHub Pages still serves them locally. # Any OTHER oversized file is a hard error so we never ship a broken site. 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" moved=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" rm "$f" echo "redirect: $rel -> $cdn" moved=$((moved + 1)) done < <(find site -type f -size +26214400c -print0) echo "offloaded $moved oversized file(s)" echo "----- site/_redirects -----" cat "$redirects" - name: Deploy to Cloudflare Pages id: deploy uses: cloudflare/wrangler-action@v4 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # --branch is anything other than the project's production branch, so # every run here is a preview deployment with its own URL. command: >- pages deploy site --project-name=geolibre-preview --branch="${{ github.head_ref || github.ref_name }}" --commit-dirty=true - name: Comment preview URL on PR if: github.event_name == 'pull_request' uses: actions/github-script@v9 env: DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} with: script: | const url = process.env.DEPLOY_URL; if (!url) return; const marker = ''; const body = `${marker}\n### ⚡ Cloudflare Pages preview\n\n| Item | Value |\n| --- | --- |\n| Preview | ${url} |\n| Demo app | ${url}/demo/ |\n| Commit | \`${context.sha.slice(0, 7)}\` |`; const { owner, repo } = context.repo; const issue_number = context.issue.number; const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 }); const existing = comments.find((c) => c.body && c.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); } else { await github.rest.issues.createComment({ owner, repo, issue_number, body }); }