Preview every pull request's client at pr-N.preview.<domain> #9

Closed
opened 2026-10-12 09:54:47 +09:00 by bot-genji1024 · 0 comments
Collaborator

Follow-up to #7 (CI now uploads client/dist as the client-dist artifact, readable without a token).

Goal

Every open pull request into main gets its client build served at https://pr-N.preview.<domain>/, talking to the demo's homeserver. Nothing on the host runs from a workflow, and nothing from the pull request runs on the host: the host fetches the already-built static files.

Design

New directory docs/preview/ (set up like docs/traefik/), run by the demo's user:

  • fetch.sh, run in a throwaway container (Alpine + jq; busybox wget/unzip), no Docker socket, read-only root, all capabilities dropped, /tmp a size-limited tmpfs. It lists open pull requests into main through the API, finds the latest successful ci.yml run for each head commit and its client-dist artifact, and when that changed:
    • refuses artifacts over a size limit, unzips inside the tmpfs (a zip bomb runs out of space), and rejects anything but regular files and directories (no symlinks) and builds without index.html;
    • writes the preview's own config.json (demo homeserver, no push gateway) and a public _preview.json (pull request, commit, run) so reviewers can see which commit they are looking at;
    • swaps the new tree in place of the old one.
      Previews of closed pull requests are deleted. At most PREVIEW_MAX (default 5) previews, most recently updated first. An optional PREVIEW_AUTHORS limits previews to listed authors; runs that needed approval only exist once a maintainer approved them.
  • sync.sh on the host: runs the fetch container, then builds the Traefik rule Host(pr-N.…) || … from the preview directories and (re)creates the nginx container with it, or removes the container when there are none. Per-host rules let the demo's Traefik get each certificate through TLS-ALPN, so no wildcard certificate and no DNS API token.
  • nginx (nginx.conf.template) serves pr-N from a read-only mount, with the client's caching rules and headers set by the server, which a pull request cannot change: a CSP (connect-src limited to the demo homeserver, form-action 'self', frame-ancestors 'none', …), X-Robots-Tag: noindex, Referrer-Policy: no-referrer. Dotfiles are not served.
  • systemd user timer every 2 minutes (README).

Outside this repository: a DNS-only wildcard record *.preview.<domain> and an HostSNIRegexp on the front proxy's passthrough route (koto-edge example updated alongside).

Security notes (for the README)

  • Previews live under the same registrable domain as the other services; they are separate origins, so they cannot read other sites' cookies or storage, but their scripts can set cookies for the parent domain. The CSP limits where they can send requests; it does not stop a page from navigating away with data in the URL.
  • Logging into a preview hands that pull request's code an access token for the demo homeserver: never log in as a server admin on a preview from an untrusted author.
  • This does not fix the existing exposure of the koto-demo runner label (any pull request workflow can request it); that is a separate issue.

Out of scope

Posting the URL on the pull request (needs a write token), push notifications in previews, server-side (server/) changes.

Follow-up to #7 (CI now uploads `client/dist` as the `client-dist` artifact, readable without a token). ## Goal Every open pull request into `main` gets its client build served at `https://pr-N.preview.<domain>/`, talking to the demo's homeserver. Nothing on the host runs from a workflow, and nothing from the pull request runs on the host: the host fetches the already-built static files. ## Design New directory `docs/preview/` (set up like `docs/traefik/`), run by the demo's user: - **`fetch.sh`**, run in a throwaway container (Alpine + jq; busybox wget/unzip), no Docker socket, read-only root, all capabilities dropped, `/tmp` a size-limited tmpfs. It lists open pull requests into `main` through the API, finds the latest successful `ci.yml` run for each head commit and its `client-dist` artifact, and when that changed: - refuses artifacts over a size limit, unzips inside the tmpfs (a zip bomb runs out of space), and rejects anything but regular files and directories (no symlinks) and builds without `index.html`; - writes the preview's own `config.json` (demo homeserver, no push gateway) and a public `_preview.json` (pull request, commit, run) so reviewers can see which commit they are looking at; - swaps the new tree in place of the old one. Previews of closed pull requests are deleted. At most `PREVIEW_MAX` (default 5) previews, most recently updated first. An optional `PREVIEW_AUTHORS` limits previews to listed authors; runs that needed approval only exist once a maintainer approved them. - **`sync.sh`** on the host: runs the fetch container, then builds the Traefik rule `Host(pr-N.…) || …` from the preview directories and (re)creates the nginx container with it, or removes the container when there are none. Per-host rules let the demo's Traefik get each certificate through TLS-ALPN, so no wildcard certificate and no DNS API token. - **nginx** (`nginx.conf.template`) serves `pr-N` from a read-only mount, with the client's caching rules and headers set by the server, which a pull request cannot change: a CSP (`connect-src` limited to the demo homeserver, `form-action 'self'`, `frame-ancestors 'none'`, …), `X-Robots-Tag: noindex`, `Referrer-Policy: no-referrer`. Dotfiles are not served. - **systemd user timer** every 2 minutes (README). Outside this repository: a DNS-only wildcard record `*.preview.<domain>` and an `HostSNIRegexp` on the front proxy's passthrough route (koto-edge example updated alongside). ## Security notes (for the README) - Previews live under the same registrable domain as the other services; they are separate origins, so they cannot read other sites' cookies or storage, but their scripts can set cookies for the parent domain. The CSP limits where they can send requests; it does not stop a page from navigating away with data in the URL. - Logging into a preview hands that pull request's code an access token for the demo homeserver: never log in as a server admin on a preview from an untrusted author. - This does not fix the existing exposure of the `koto-demo` runner label (any pull request workflow can request it); that is a separate issue. ## Out of scope Posting the URL on the pull request (needs a write token), push notifications in previews, server-side (`server/`) changes.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
genji1024/koto#9
No description provided.