diff --git a/.cursor/rules/demos-pages.mdc b/.cursor/rules/demos-pages.mdc new file mode 100644 index 0000000..43d2c6a --- /dev/null +++ b/.cursor/rules/demos-pages.mdc @@ -0,0 +1,42 @@ +--- +description: OpenAPI demo publishing — token safety, Pages assemble, CI trust +globs: demos/**,.github/workflows/pages*.yml +alwaysApply: false +--- + +# Demos and GitHub Pages + +Source of truth: `demos/README.md` and `demos/assemble-pages.sh`. Keep the README inclusion rules identical to the script `if`s. + +## Token + +- Client-only: OAT in memory only; send only as `x-api-key` to hosts in `config` / OpenAPI `servers`. +- No `sessionStorage` / `localStorage` / cookies / analytics / CDN scripts that can read the token. +- `fetch` that carries the OAT: `redirect: 'error'`. +- Do not official-host a backend that accepts someone else's OAT. + +## Assemble script (`demos/assemble-pages.sh`) + +- Guard `rm -rf -- "$out"`: canonicalize `out` (`abspath` + `normpath`), then refuse empty, `.`, `..`, `/`, leading `-`, repo root, `demos/`, or an `out` that is an ancestor of the repo. +- Demo folder names: `A–Z a–z 0–9 . _ -` only; skip the rest (they go into HTML). +- Publish when there is no `vercel.json` and either: + - no `package.json` + root `index.html` → copy the **whole folder**; + - `package.json` + `package-lock.json` (or `npm-shrinkwrap.json`) → `rm -rf dist && npm ci --no-audit --no-fund && npm run build`, publish `dist/` (must contain `dist/index.html`). Fail fast if the lockfile is missing. +- Vite: `base: './'`. Do not commit `node_modules/` or `dist/`. + +## Workflow + +- PR: assemble only, never `deploy-pages`. +- Deploy only from `master` (push or `workflow_dispatch` on that ref). +- Do not run `npm` / assemble on **fork** PRs (`head.repo.full_name != github.repository`). Same-repo PRs may assemble. +- Generic `upload-artifact` of `_site` must set `include-hidden-files: true` so `.nojekyll` reaches Pages. +- One PR should not both invent a new CI trust path and change publish layout. Split those. + +## Before opening a PR + +```bash +bash demos/assemble-pages.sh _site +npx --yes shellcheck demos/assemble-pages.sh +``` + +Then re-read `demos/README.md` against the script. Serve `_site` locally if the change is user-visible. diff --git a/.github/workflows/pages-demos.yml b/.github/workflows/pages-demos.yml index 12ab3d9..3114a94 100644 --- a/.github/workflows/pages-demos.yml +++ b/.github/workflows/pages-demos.yml @@ -6,27 +6,52 @@ on: paths: - 'demos/**' - '.github/workflows/pages-demos.yml' + pull_request: + paths: + - 'demos/**' + - '.github/workflows/pages-demos.yml' workflow_dispatch: -permissions: - contents: read - pages: write - id-token: write - concurrency: - group: pages + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true jobs: + assemble: + # Fork PRs can run arbitrary npm scripts; only build same-repo PRs and master. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Assemble site + run: bash demos/assemble-pages.sh _site + - uses: actions/upload-artifact@v4 + with: + name: site + path: _site + include-hidden-files: true + deploy: + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' + needs: assemble environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write steps: - - uses: actions/checkout@v4 - - name: Assemble site - run: bash demos/assemble-pages.sh _site + - uses: actions/download-artifact@v4 + with: + name: site + path: _site - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v3 with: diff --git a/demos/README.md b/demos/README.md index 2c03fd2..0f85240 100644 --- a/demos/README.md +++ b/demos/README.md @@ -23,7 +23,10 @@ Each demo should document how the caller supplies their own token. ## Catalog -Official static host: [https://robotemi.github.io/openapi/](https://robotemi.github.io/openapi/) (index of every client-only demo). A folder is published when it has `index.html` and no `vercel.json`. +Official static host: [https://robotemi.github.io/openapi/](https://robotemi.github.io/openapi/). A folder is published when it has no `vercel.json` and either: + +- **No build:** root `index.html` and no `package.json` — the **whole folder** is copied (HTML, JS, README, assets). +- **npm build:** `package.json` plus `package-lock.json` (or `npm-shrinkwrap.json`). CI runs `rm -rf dist && npm ci --no-audit --no-fund && npm run build` and publishes only `dist/` (must contain `dist/index.html`). A source `index.html` is only needed if the bundler uses it (Vite does). | Demo | Mode | Deploy | |------|------|--------| @@ -34,7 +37,9 @@ Official static host: [https://robotemi.github.io/openapi/](https://robotemi.git 1. Add a self-contained directory under `demos/`. 2. Include a short `README.md` with purpose, setup, and how the OAT is provided. 3. Keep dependencies and scope small enough that someone else can run the demo without a private environment. -4. Client-only static demos: add `index.html` at the folder root, omit `vercel.json`, and use a URL-safe folder name (`A–Z a–z 0–9 . _ -`). Merge to `master` and the Pages workflow copies the folder to `https://robotemi.github.io/openapi//`. -5. Serverless demos: do not add `index.html` as a Pages app (or include `vercel.json`). Deploy as a separate Vercel project. +4. Client-only static demos (no build): add `index.html` at the folder root, omit `package.json` and `vercel.json`, and use a URL-safe folder name (`A–Z a–z 0–9 . _ -`). Merge to `master` and the Pages workflow copies the **entire folder** to `https://robotemi.github.io/openapi//`. +5. Client-only demos that use Vite/TypeScript: commit `package.json` **and** a lockfile (`package-lock.json` or `npm-shrinkwrap.json`), with `npm run build` writing `dist/index.html`. Set Vite `base` to `./`. Omit `vercel.json`. Do not commit `node_modules/` or `dist/`. Pages runs `rm -rf dist && npm ci --no-audit --no-fund && npm run build` and publishes `dist/`. +6. Serverless demos: include `vercel.json`. Deploy as a separate Vercel project. +7. Pull-request assemble (build, no publish) runs only for branches on `robotemi/openapi`. Fork PRs are not built automatically; a maintainer can check out the branch and run `bash demos/assemble-pages.sh _site` locally. Questions about the API itself belong in the [API reference](https://openapi-docs.robotemi.com). diff --git a/demos/assemble-pages.sh b/demos/assemble-pages.sh index ca01e43..51c7973 100755 --- a/demos/assemble-pages.sh +++ b/demos/assemble-pages.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Build the GitHub Pages tree: index + each static demo as a subfolder. -# A demo is published when it has index.html and no vercel.json. +# A demo is published when it has no vercel.json and either: +# - package.json + lockfile → rm -rf dist && npm ci --no-audit --no-fund && npm run build, publish dist/ +# - root index.html (no package.json) → copy the whole folder set -euo pipefail demos="$(cd "$(dirname "$0")" && pwd)" @@ -17,10 +19,7 @@ case "$out" in exit 1 ;; esac -if [ "${out#/}" = "$out" ]; then - out="$(pwd)/$out" -fi -out="${out%/}" +out="$(python3 -c 'import os, sys; print(os.path.normpath(os.path.abspath(os.path.expanduser(sys.argv[1]))))' "$out")" if [ "$out" = "$repo" ] || [ "$out" = "$demos" ]; then echo "assemble-pages: refusing to delete the repo or demos directory" >&2 exit 1 @@ -59,7 +58,6 @@ touch -- "$out/.nojekyll" EOF for dir in "$demos"/*/; do [ -d "$dir" ] || continue - [ -f "${dir}index.html" ] || continue [ -f "${dir}vercel.json" ] && continue name="$(basename "$dir")" case "$name" in @@ -68,7 +66,26 @@ EOF continue ;; esac - cp -R -- "$dir" "$out/$name" + if [ -f "${dir}package.json" ]; then + if [ ! -f "${dir}package-lock.json" ] && [ ! -f "${dir}npm-shrinkwrap.json" ]; then + echo "assemble-pages: $name has package.json but no package-lock.json (or npm-shrinkwrap.json); npm ci requires a lockfile" >&2 + exit 1 + fi + echo "assemble-pages: building $name" + (cd "$dir" && rm -rf dist && npm ci --no-audit --no-fund && npm run build) + if [ ! -f "${dir}dist/index.html" ]; then + echo "assemble-pages: $name build produced no dist/index.html" >&2 + exit 1 + fi + mkdir -p -- "$out/$name" + cp -R -- "${dir}dist/." "$out/$name/" + touch -- "$out/$name/.nojekyll" + elif [ -f "${dir}index.html" ]; then + cp -R -- "$dir" "$out/$name" + else + echo "assemble-pages: skipping $name (need index.html, or package.json + lockfile)" >&2 + continue + fi printf '
  • %s
  • \n' "$name" "$name" done cat <<'EOF'