Skip to content

Commit d1522ac

Browse files
authored
Merge pull request #919 from quarto-dev/feature/bd-73lilznv-move-sandboxed-preview-quarto
Move sandboxed preview to quarto-dev.github.io/q2-sandbox (bd-73lilznv)
2 parents d49ad93 + 19c5745 commit d1522ac

9 files changed

Lines changed: 341 additions & 18 deletions

File tree

‎.claude/skills/snyk-pr/references/katex.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ bd-4b7f1hr7). Snyk bumps only surface 2 — every katex Snyk PR arrives red.
3535
a committed single-file bundle with KaTeX inlined (the surface PR #571 missed,
3636
repaired in PR #573). Since `5684cfead` (2026-09-01) the sandboxed preview
3737
builds into the gitignored `hub-client/public/q2-sandboxed-preview/` and is
38-
deployed by `.github/workflows/deploy-sandboxed-preview.yml`, so there is no
38+
deployed by `.github/workflows/deploy-q2-sandbox.yml`, so there is no
3939
committed bundle to regenerate. If a committed copy ever reappears, add it back
4040
here.
4141

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Deploy sandboxed preview to quarto-dev/q2-sandbox
2+
3+
# Publishes the sandboxed preview iframe to
4+
# https://quarto-dev.github.io/q2-sandbox/ by force-pushing the built dist/
5+
# as a single commit to the gh-pages branch of quarto-dev/q2-sandbox (Pages
6+
# there serves that branch). A workflow cannot deploy Pages for another repo
7+
# via actions/deploy-pages, hence the push. Auth is the write deploy key
8+
# scoped to q2-sandbox only (secret Q2_SANDBOX_DEPLOY_KEY). See
9+
# .github/workflows/github-pages.md.
10+
11+
on:
12+
push:
13+
branches: [main]
14+
paths:
15+
- 'hub-client/quarto-hub-sandboxed-preview/**'
16+
# The bundle embeds the renderer from preview-renderer source and
17+
# vendored resources; rebuild when they change.
18+
- 'ts-packages/preview-renderer/**'
19+
- 'resources/js/**'
20+
- 'resources/revealjs/**'
21+
- 'resources/attribution/**'
22+
- '.github/workflows/deploy-q2-sandbox.yml'
23+
workflow_dispatch:
24+
25+
permissions:
26+
contents: read
27+
28+
# Allow only one concurrent deployment; don't cancel in-progress runs.
29+
concurrency:
30+
group: q2-sandbox-pages
31+
cancel-in-progress: false
32+
33+
jobs:
34+
deploy:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- name: Checkout Repo
38+
uses: actions/checkout@v6
39+
40+
- name: Setup Node
41+
uses: actions/setup-node@v6
42+
with:
43+
node-version: '24'
44+
cache: 'npm'
45+
cache-dependency-path: |
46+
hub-client/quarto-hub-sandboxed-preview/package-lock.json
47+
package-lock.json
48+
49+
# The bundle imports @quarto/preview-renderer *source*, whose bare
50+
# deps (tiptap, reveal.js, katex, …) resolve from the repo-root
51+
# node_modules — install the workspaces first.
52+
- name: Install workspace dependencies
53+
run: npm ci
54+
55+
# This package is deliberately a standalone npm project (own lockfile),
56+
# not part of the root npm workspaces — install inside it too.
57+
- name: Install dependencies
58+
run: npm ci
59+
working-directory: hub-client/quarto-hub-sandboxed-preview
60+
61+
- name: Build
62+
run: npm run build
63+
working-directory: hub-client/quarto-hub-sandboxed-preview
64+
65+
- name: Push dist to quarto-dev/q2-sandbox gh-pages
66+
working-directory: hub-client/quarto-hub-sandboxed-preview/dist
67+
env:
68+
DEPLOY_KEY: ${{ secrets.Q2_SANDBOX_DEPLOY_KEY }}
69+
run: |
70+
set -euo pipefail
71+
key="$RUNNER_TEMP/q2-sandbox-deploy-key"
72+
(umask 077 && printf '%s\n' "$DEPLOY_KEY" > "$key")
73+
export GIT_SSH_COMMAND="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
74+
# GitHub Pages runs Jekyll on branch deploys unless told not to.
75+
touch .nojekyll
76+
git init -q -b gh-pages
77+
git add -A
78+
git -c user.name='github-actions[bot]' \
79+
-c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
80+
commit -q -m "Deploy quarto-dev/q2@${GITHUB_SHA}"
81+
git push --force git@github.com:quarto-dev/q2-sandbox.git gh-pages
82+
rm -f "$key"

‎.github/workflows/deploy-sandboxed-preview.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
name: Deploy sandboxed preview to GitHub Pages
22

3+
# TRANSITIONAL (bd-73lilznv): the sandbox now lives at
4+
# https://quarto-dev.github.io/q2-sandbox/ (deploy-q2-sandbox.yml). This copy
5+
# at /q2/ only serves quarto-hub.com builds that predate the new default, and
6+
# is deleted once production is cut over so /q2/ can host the Q2 docs.
7+
38
on:
49
push:
510
branches: [main]

‎.github/workflows/github-pages.md‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
# GitHub Pages deployment
22

3-
`deploy-sandboxed-preview.yml` builds `hub-client/quarto-hub-sandboxed-preview/`
3+
## Sandboxed preview → https://quarto-dev.github.io/q2-sandbox/
4+
5+
`deploy-q2-sandbox.yml` builds `hub-client/quarto-hub-sandboxed-preview/`
46
(root `npm ci` for the workspace deps the renderer source needs, then
57
`npm ci` + `npm run build` inside the package) and publishes its `dist/` —
6-
`index.html`, hashed `assets/` (renderer bundle, KaTeX fonts), and
7-
`serviceWorker.js` — to **https://quarto-dev.github.io/q2/**,
8+
`index.html`, hashed `q2-preview-assets/` (renderer bundle, KaTeX fonts), and
9+
`serviceWorker.js` — to **https://quarto-dev.github.io/q2-sandbox/**,
810
where hub-client's `Q2SandboxedPreviewIframe.tsx` loads it cross-origin as its
9-
iframe `src` (override with `VITE_Q2_SANDBOXED_PREVIEW_URL`). It runs on any
10-
push to `main` touching that package, or manually via
11-
`gh workflow run deploy-sandboxed-preview.yml`; Pages is enabled on this repo
12-
in "GitHub Actions" mode, and the `/q2/` path is fixed by GitHub (project
13-
Pages sites always live at `https://<org>.github.io/<repo>/`). The page is
11+
iframe `src` (override with `VITE_Q2_SANDBOXED_PREVIEW_URL`). The page is
1412
inert when visited directly — it only resolves content via `postMessage` to a
1513
parent frame.
14+
15+
A workflow cannot deploy Pages for another repository, so the build runs here
16+
and the result is force-pushed as a single commit to the `gh-pages` branch of
17+
[`quarto-dev/q2-sandbox`](https://github.com/quarto-dev/q2-sandbox), whose
18+
Pages site serves that branch (with `.nojekyll`). Auth is a write deploy key
19+
on `q2-sandbox` only; its private half is this repo's
20+
`Q2_SANDBOX_DEPLOY_KEY` secret. To rotate it, generate a new ed25519 key,
21+
replace the deploy key on `q2-sandbox`, and `gh secret set
22+
Q2_SANDBOX_DEPLOY_KEY -R quarto-dev/q2`.
23+
24+
It runs on any push to `main` touching the sandbox package, the
25+
preview-renderer source, or the vendored resources it embeds, or manually via
26+
`gh workflow run deploy-q2-sandbox.yml`.
27+
28+
## This repo's own Pages site → https://quarto-dev.github.io/q2/
29+
30+
Pages on this repo is in "GitHub Actions" mode (no `gh-pages` branch; it
31+
serves whatever artifact the last deploy workflow uploaded), and the `/q2/`
32+
path is fixed by GitHub (project Pages sites always live at
33+
`https://<org>.github.io/<repo>/`). It is being freed for the Q2
34+
documentation (bd-73lilznv,
35+
`claude-notes/plans/2026-10-26-move-sandbox-free-q2-pages.md`). Until
36+
production quarto-hub.com is rebuilt against the `/q2-sandbox/` default,
37+
`deploy-sandboxed-preview.yml` keeps publishing the same sandbox bundle here
38+
too; it is deleted once the cutover is verified.
39+
40+
Note that both sites share the `quarto-dev.github.io` origin; giving the
41+
sandbox an origin of its own is bd-gotwa715.

‎claude-notes/designs/q2-sandboxed-preview-separate-domain.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@ Main app: https://your-hub-domain.com/
2020
├─ Serves the main React app (owns WASM + VFS)
2121
└─ Q2SandboxedPreviewIframe (parent side of the protocol)
2222
23-
Sandbox: https://quarto-dev.github.io/q2/ (GitHub Pages)
23+
Sandbox: https://quarto-dev.github.io/q2-sandbox/ (GitHub Pages, quarto-dev/q2-sandbox)
2424
├─ index.html + q2-preview-assets/* (renderer bundle, KaTeX fonts)
2525
├─ serviceWorker.js (asset proxy)
2626
└─ Communicates ONLY via postMessage
2727
```
2828

29-
- Deployment: `.github/workflows/deploy-sandboxed-preview.yml` publishes
30-
`hub-client/quarto-hub-sandboxed-preview/dist/` to GitHub Pages (see
31-
`.github/workflows/github-pages.md`).
29+
- Deployment: `.github/workflows/deploy-q2-sandbox.yml` builds
30+
`hub-client/quarto-hub-sandboxed-preview/dist/` and pushes it to the
31+
`gh-pages` branch of `quarto-dev/q2-sandbox` (see
32+
`.github/workflows/github-pages.md`). Until 2026-10 it was served from
33+
`/q2/`, which now belongs to the Q2 docs (bd-73lilznv). The origin is
34+
still shared with other `quarto-dev.github.io` sites (bd-gotwa715).
3235
- Local dev fallback: the build also copies dist/ to
3336
`hub-client/public/q2-sandboxed-preview/` (gitignored); point
3437
`VITE_Q2_SANDBOXED_PREVIEW_URL=q2-sandboxed-preview/index.html` at it.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
title: 'Move the sandboxed preview to quarto-dev.github.io/q2-sandbox and free /q2 for the Q2 docs'
3+
date: 2026-10-26
4+
description: 'quarto-dev.github.io/q2 serves the q2-sandboxed-preview iframe today, and the live quarto-hub.com bundle points at it. Move the sandbox to a new repo quarto-dev/q2-sandbox, repoint hub-client, retire the old Pages workflow, then publish the Q2 docs at /q2/ with a self-unregistering service worker that cleans up stale sandbox workers.'
5+
---
6+
7+
Strand: **bd-73lilznv**. Follow-up (out of scope here): **bd-gotwa715**, which
8+
gives the sandbox its own origin.
9+
10+
## Overview
11+
12+
### Current state (verified 2026-10-26)
13+
14+
- `quarto-dev/q2` has GitHub Pages in **GitHub Actions mode**
15+
(`build_type: workflow`), so there is no `gh-pages` branch. Pages serves
16+
whatever artifact the last workflow run uploaded.
17+
- That workflow is `.github/workflows/deploy-sandboxed-preview.yml`. It builds
18+
`hub-client/quarto-hub-sandboxed-preview/` and deploys `dist/` on pushes to
19+
`main` that touch the sandbox package, `ts-packages/preview-renderer/**`,
20+
`resources/js|revealjs|attribution/**`, or the workflow itself. The
21+
`github-pages` environment allows deploys from `main` only (custom branch
22+
policy).
23+
- `Q2SandboxedPreviewIframe.tsx:67` defaults
24+
`VITE_Q2_SANDBOXED_PREVIEW_URL` to `https://quarto-dev.github.io/q2/`.
25+
- **Production** (`quarto-dev/quarto-hub-deployment`,
26+
`.github/workflows/build-and-deploy.yml`) does not override the variable.
27+
The live quarto-hub.com bundle contains `quarto-dev.github.io/q2/`. The
28+
`q2 preview --ui editor` embed (`npm run build:preview-embed` in
29+
`release-pipeline.yml`) uses the same default.
30+
- The iframe only loads for documents with `format: q2-sandboxed-preview`.
31+
- e2e, `scripts/local-dev.sh`, and `hub-client-e2e.yml` all override the URL,
32+
so this move doesn't affect them.
33+
34+
### Decisions (user, 2026-10-26)
35+
36+
- The new home is **`https://quarto-dev.github.io/q2-sandbox/`**, served from
37+
a new repo `quarto-dev/q2-sandbox`. The name is free (404 on the API as of
38+
today).
39+
- Breaking `q2-sandboxed-preview` in already-released q2 binaries is
40+
acceptable (early development). A changelog line is enough.
41+
- The new URL has the same origin as the docs (`quarto-dev.github.io`), which
42+
is a known weakness. It is no worse than today, and bd-gotwa715 tracks the
43+
fix.
44+
45+
### Hazard: stale service workers at scope `/q2/`
46+
47+
`quarto-hub-sandboxed-preview/src/entry.tsx:400` calls `initServiceWorker()`
48+
on every page load, including a direct top-level visit. The worker
49+
(`serviceWorker.ts`) intercepts **every** in-scope GET except `''`,
50+
`index.html`, `serviceWorker.js` and `q2-preview-assets/*`, and proxies it to
51+
the parent frame. A top-level navigation has no `clientId`, so the proxy
52+
answers **502 "No client for VFS proxy request"**.
53+
54+
So anyone who ever opened `https://quarto-dev.github.io/q2/` directly would
55+
get 502s on every docs page except the landing page. Workers registered
56+
inside the quarto-hub.com iframe are partitioned by top-level site in current
57+
browsers and don't affect direct visits.
58+
59+
Removing `serviceWorker.js` (a 404) does **not** unregister the old worker.
60+
The docs site must ship a replacement `serviceWorker.js` at the same path.
61+
Its job is to activate, unregister itself, and reload any open pages.
62+
Browsers check for a worker update on in-scope navigations, and the landing
63+
page (`index.html`, exempt from interception) always loads from the network,
64+
so recovery is quick. The landing page also gets a small unregister snippet
65+
as a backup.
66+
67+
## Work items
68+
69+
### Phase 1: Create `quarto-dev/q2-sandbox` and deploy to it
70+
71+
The q2 workflow can't deploy Pages for another repo through
72+
`actions/deploy-pages`. **Recommended:** keep the build in q2, where the
73+
trigger paths already live, and push `dist/` to the `gh-pages` branch of
74+
`q2-sandbox` with a deploy key. The alternative, a workflow in `q2-sandbox`
75+
triggered by `repository_dispatch` that checks out q2 and builds, splits the
76+
build config across two repos. Confirm the choice before Phase 1 runs.
77+
78+
- [ ] Create public repo `quarto-dev/q2-sandbox` with a README explaining
79+
that it's a deploy target and the source lives in
80+
`quarto-dev/q2/hub-client/quarto-hub-sandboxed-preview`. *(Outward-facing:
81+
confirm with the user before running `gh repo create`.)*
82+
- [ ] Generate an SSH deploy key pair. Add the public key to `q2-sandbox`
83+
with write access, and the private key to `quarto-dev/q2` as secret
84+
`Q2_SANDBOX_DEPLOY_KEY`. *(Confirm before writing secrets. The user
85+
may prefer to do this by hand.)*
86+
- [ ] Add a new workflow in q2, `.github/workflows/deploy-q2-sandbox.yml`.
87+
It uses the same triggers and build steps as
88+
`deploy-sandboxed-preview.yml`, adds `.nojekyll` to `dist/`, and
89+
pushes `dist/` to `q2-sandbox` `gh-pages` (force-push a single commit,
90+
e.g. `peaceiris/actions-gh-pages` with `external_repository` +
91+
`deploy_key`, pinned to a version at least 2 weeks old). It needs only
92+
`contents: read`, with no `pages`/`id-token`.
93+
- [ ] After the first push, enable Pages on `q2-sandbox` from the `gh-pages`
94+
branch (`gh api -X POST repos/quarto-dev/q2-sandbox/pages ...`).
95+
- [ ] Verify that `https://quarto-dev.github.io/q2-sandbox/`,
96+
`/q2-sandbox/serviceWorker.js` and a `q2-preview-assets/` chunk all
97+
return 200 with the right content types.
98+
99+
Run the old and new workflows side by side until Phase 3. `/q2/` keeps
100+
serving the sandbox until production stops using it.
101+
102+
### Phase 2: Repoint hub-client (q2 PR)
103+
104+
- [ ] `hub-client/src/components/render/q2-sandboxed-preview/Q2SandboxedPreviewIframe.tsx:67`:
105+
change the default to `https://quarto-dev.github.io/q2-sandbox/` and
106+
update the comment above it.
107+
- [ ] `assetPolicy.test.ts`: update `PAGES_SCOPE` (line 20) and `base` (line
108+
113). Check for any other `/q2/` literals the tests depend on.
109+
- [ ] Docs and notes: rewrite `.github/workflows/github-pages.md` to cover
110+
the new split (q2 Pages = docs, q2-sandbox = sandbox). Update
111+
`claude-notes/designs/q2-sandboxed-preview-separate-domain.md`
112+
(architecture diagram and deployment bullet) and
113+
`.claude/skills/snyk-pr/references/katex.md` (workflow name). Leave
114+
the historical 2026-09-01 port plan as it is.
115+
- [ ] Add a `hub-client/changelog.md` entry.
116+
- [ ] Origin pinning doesn't change: `sandboxTargetOrigin()` derives
117+
`https://quarto-dev.github.io` from both URLs.
118+
- [ ] Tests: `npm run test` in hub-client (assetPolicy unit tests) and the
119+
`q2-sandboxed-preview` e2e spec, which uses the same-origin copy and
120+
should be unaffected.
121+
- [ ] Merge to `main`. *(Push and PR only with explicit go-ahead.)*
122+
123+
### Phase 3: Production cutover
124+
125+
- [ ] Trigger a quarto-hub.com rebuild from `quarto-hub-deployment` so it
126+
picks up the new default from q2 `main`. *(Confirm before triggering
127+
a production deploy.)*
128+
- [ ] Verify the live bundle contains `quarto-dev.github.io/q2-sandbox/` and
129+
no longer contains `quarto-dev.github.io/q2/`, using the same
130+
curl-and-grep against `https://quarto-hub.com/` as before.
131+
- [ ] Smoke test: open a `format: q2-sandboxed-preview` doc on quarto-hub.com.
132+
The iframe should load from `/q2-sandbox/`, an image asset should
133+
render through the SW proxy, and the console should be clean.
134+
135+
### Phase 4: Retire the old workflow and publish docs at `/q2/`
136+
137+
- [ ] Delete `.github/workflows/deploy-sandboxed-preview.yml`. Leave Pages
138+
on `quarto-dev/q2` in GitHub Actions mode.
139+
- [ ] Add a docs deploy workflow (e.g. `.github/workflows/deploy-docs.yml`)
140+
that builds q2, runs `cargo xtask stage-doc-examples`, renders `docs/`
141+
with **Q2** (`q2 render docs/`, not Q1), and deploys `docs/_site/` via
142+
`upload-pages-artifact` + `deploy-pages` to the existing
143+
`github-pages` environment. Trigger it on `main` pushes touching
144+
`docs/**`, plus `workflow_dispatch`. Decide during implementation
145+
whether a pinned q2 release binary or a source build is the better
146+
renderer for CI; the release pipeline's docs-embed step is prior art.
147+
Set `site-url: https://quarto-dev.github.io/q2/` if the site needs it
148+
(sitemap, llms.txt).
149+
- [ ] Add a self-unregistering `serviceWorker.js` to the docs output root
150+
(e.g. `docs/serviceWorker.js` listed under `project.resources`):
151+
152+
```js
153+
self.addEventListener('install', () => self.skipWaiting());
154+
self.addEventListener('activate', (e) => e.waitUntil((async () => {
155+
await self.registration.unregister();
156+
for (const c of await self.clients.matchAll({ type: 'window' })) c.navigate(c.url);
157+
})()));
158+
```
159+
160+
Add a comment explaining why it exists and when it can go (no earlier
161+
than about 2027-04).
162+
- [ ] Add a backup unregister snippet to the docs landing page: on load,
163+
unregister any `navigator.serviceWorker` registration whose scope is
164+
`/q2/`.
165+
- [ ] Verification: in a browser profile, open the **old** sandbox page at
166+
`/q2/` top-level before the docs deploy, so its worker registers.
167+
Deploy the docs, then visit a docs subpage. Expect at most one 502
168+
followed by recovery, or none if the landing page is visited first.
169+
Confirm in `chrome://serviceworker-internals` that the registration
170+
is gone. If no pre-deploy profile was captured, record that the step
171+
was skipped.
172+
173+
## Out of scope
174+
175+
- Separate origin for the sandbox: **bd-gotwa715**.
176+
- Stopping `entry.tsx` from registering the worker when not framed. This is
177+
noted on bd-gotwa715. It matters less once `/q2-sandbox/` has no
178+
first-party visitors besides us.
179+
180+
## Progress log
181+
182+
- **2026-10-26: Phase 1 GitHub setup done.** Created `quarto-dev/q2-sandbox`
183+
(public), added a write deploy key (title `quarto-dev/q2
184+
deploy-q2-sandbox.yml`), and set secret `Q2_SANDBOX_DEPLOY_KEY` on
185+
`quarto-dev/q2`. The local key was deleted right after. The first
186+
`gh-pages` commit was pushed **by hand** from a local `dist/` build (with
187+
`.nojekyll`), and Pages is enabled from `gh-pages` /. Verified 200 with
188+
correct content types for `/`, `index.html`, `serviceWorker.js`
189+
(byte-identical to the local build), the JS/CSS chunks and a KaTeX woff2.
190+
The CI workflow `deploy-q2-sandbox.yml` (plain git + deploy key, no
191+
third-party action) is written and passes actionlint. It has not run yet
192+
and first runs when this branch merges to `main`.
193+
- **2026-10-26: Phase 2 code done on branch
194+
`braid/bd-73lilznv-move-sandboxed-preview-quarto`.** Changed the default
195+
URL, updated assetPolicy tests (plus a `/q2/` vs `/q2-sandbox/`
196+
scope-prefix case), `github-pages.md`, the design doc and the snyk katex
197+
reference, and marked `deploy-sandboxed-preview.yml` TRANSITIONAL. It
198+
keeps running until Phase 4. hub-client `npm run test` passes
199+
(1686/1686) and `tsc -b` is clean.

‎hub-client/changelog.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ WASM rebuild is needed for a changelog-only edit.
2323
2424
-->
2525

26+
### 2026-10-26
27+
28+
- [`be6300e0f`](https://github.com/quarto-dev/q2/commits/be6300e0f): The `q2-sandboxed-preview` iframe now loads from `https://quarto-dev.github.io/q2-sandbox/` instead of `https://quarto-dev.github.io/q2/`, which will host the Quarto 2 documentation.
29+
2630
### 2026-10-23
2731

2832
- [`ed9fbe476`](https://github.com/quarto-dev/q2/commits/ed9fbe476): The replay drawer is redesigned: it's now called "Version history", the history waveform follows the theme with the unreplayed portion faded and a clear playhead, transport buttons are proper icons with keyboard shortcuts in their tooltips, and restoring an older state is a prominent "Restore this version" button.

0 commit comments

Comments
 (0)