From fbd08dd86e087afaefcc9794b23d59226227f8e0 Mon Sep 17 00:00:00 2001 From: Tim de Groot Date: Sat, 26 Sep 2026 01:48:40 +0200 Subject: [PATCH] ci: rebuild the example dashboard for the dev docs (#358) The docs site copies the committed example/robot_dashboard.html of the version it builds. For a release that is what it should do, but /dev/ documents unreleased main, where that file is behind by every merge since the last release: it was 7 commits stale and showed none of the features its own documentation describes. Every version is built in a temporary git worktree, so the rebuild happens inside the dev worktree, before copy-static.mjs copies the example into docs/public. scripts/example.py runs the package from source with that worktree as cwd, so the example is produced by the very code the docs next to it describe, and the worktree is thrown away afterwards, leaving example/ in the checkout untouched. The deploy workflow gains a python and a pip install step, which is what the rebuild needs: the dependencies of the package, not the package. A build without a usable python keeps the committed example and says so, which is fine locally but would silently bring the stale example back in CI, so there it fails instead. --no-example (DOCS_SKIP_EXAMPLE) skips the rebuild for quick local iteration. Co-Authored-By: Claude Opus 5 --- .claude/skills/dev-workflow/SKILL.md | 1 + .claude/skills/release/SKILL.md | 2 + .github/workflows/deploy.yml | 9 +++++ CONTRIBUTING.md | 2 + scripts/docs/build-versioned-docs.mjs | 57 +++++++++++++++++++++++++-- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/.claude/skills/dev-workflow/SKILL.md b/.claude/skills/dev-workflow/SKILL.md index b7b88c22..5b717ce4 100644 --- a/.claude/skills/dev-workflow/SKILL.md +++ b/.claude/skills/dev-workflow/SKILL.md @@ -61,6 +61,7 @@ npm run docs:preview # preview the build npm run docs:build:versions -- --only latest,dev,v1.3.0 # versioned site as CI builds it (omit --only for all tags) npm run docs:preview:versions # serve dist under /robotframework-dashboard/ like Pages npm run docs:build:versions -- --cache-dir .docs-dist-cache # skip versions whose build is already cached (CI does this) +npm run docs:build:versions -- --only dev --no-example # keep the committed example instead of rebuilding it ``` `package.json` exists **only** for the docs site — it has nothing to do with bundling dashboard JS. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 27a80b40..f7225d90 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -48,6 +48,8 @@ Do not change the ASCII art banner lines above it. ## Step 3 — Regenerate the example dashboard and database The example dashboard and database live in `example/robot_dashboard.html` and `example/robot_results.db`. +They are what every *released* docs version serves; the `/dev/` docs rebuild the example from `main` on every +deploy (`scripts/docs/build-versioned-docs.mjs`), so this step is only about the release itself. Run from anywhere (any OS): diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 667aac35..26ea4c3b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -42,6 +42,15 @@ jobs: cache: npm - name: Setup Pages uses: actions/configure-pages@v6 + # The /dev/ build rebuilds example/robot_dashboard.html from its worktree, so the + # unreleased example shows what main does instead of what the last release did. Only + # the dependencies of the package are needed: scripts/example.py runs it from source. + - name: Setup Python + uses: actions/setup-python@v7 + with: + python-version: '3.12' + - name: Install the package dependencies for the dev example dashboard + run: pip install . - name: Install dependencies run: npm ci # Released versions never change: their finished builds are kept between diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a467934..7d6122d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -248,5 +248,7 @@ npm run docs:build:versions npm run docs:build:versions -- --only latest,dev,v1.3.0 npm run docs:build:versions -- --only latest,legacy # latest + all README-only releases npm run docs:build:versions -- --cache-dir .docs-dist-cache # reuse finished builds between runs, like CI +npm run docs:build:versions -- --only dev --no-example # skip rebuilding the example dashboard npm run docs:preview:versions ``` +The `dev` version rebuilds `example/robot_dashboard.html` from its own worktree first (`scripts/example.py`), so the example under `/dev/` shows what `main` does rather than what the last release did. Every released version keeps the example that was committed for it. The rebuild needs a python with the package dependencies installed; without one the build keeps the committed example and says so, and `--no-example` skips it on purpose. Neither writes into your checkout: the rebuild happens in the temporary worktree. diff --git a/scripts/docs/build-versioned-docs.mjs b/scripts/docs/build-versioned-docs.mjs index 1cca19d5..b97d8ec9 100644 --- a/scripts/docs/build-versioned-docs.mjs +++ b/scripts/docs/build-versioned-docs.mjs @@ -19,9 +19,17 @@ // node scripts/docs/build-versioned-docs.mjs --only legacy only the README-only versions // node scripts/docs/build-versioned-docs.mjs --main-ref origin/main // node scripts/docs/build-versioned-docs.mjs --cache-dir .docs-dist-cache +// node scripts/docs/build-versioned-docs.mjs --no-example keep the committed example dashboard // // Env: DOCS_MAIN_REF (same as --main-ref, default "main"), -// DOCS_CACHE_DIR (same as --cache-dir). +// DOCS_CACHE_DIR (same as --cache-dir), +// DOCS_SKIP_EXAMPLE (same as --no-example), +// PYTHON (interpreter used to rebuild the `dev` example dashboard). +// +// The example dashboard is committed and only regenerated when a release is cut, so every +// released version serves the example its own code produced. `dev` documents unreleased main, +// where that file is behind by every merge since the last release, so its example is rebuilt +// from the worktree (scripts/example.py) before the docs are built. // // Cache: released versions never change, so with a cache dir each finished // build is stored there under a fingerprint (commit + everything overlaid + @@ -71,11 +79,17 @@ const PATCHES = { }; function parse_args(argv) { - const args = { only: null, mainRef: process.env.DOCS_MAIN_REF || 'main', cacheDir: process.env.DOCS_CACHE_DIR || null }; + const args = { + only: null, + mainRef: process.env.DOCS_MAIN_REF || 'main', + cacheDir: process.env.DOCS_CACHE_DIR || null, + example: !process.env.DOCS_SKIP_EXAMPLE, + }; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--only') args.only = new Set(argv[++i].split(',').map((s) => s.trim()).filter(Boolean)); else if (argv[i] === '--main-ref') args.mainRef = argv[++i]; else if (argv[i] === '--cache-dir') args.cacheDir = argv[++i]; + else if (argv[i] === '--no-example') args.example = false; else throw new Error(`Unknown argument: ${argv[i]}`); } if (args.cacheDir) args.cacheDir = resolve(repoRoot, args.cacheDir); @@ -305,7 +319,40 @@ function strip_shared_assets(worktree, build, latest) { return shared; } -function build_one(build, env, latest) { +// scripts/example.py imports the fixtures with the source of the worktree it runs in +// (python -m, cwd first on sys.path), so the example matches the code the docs describe. +// It needs the dependencies of the package, not the package itself. +function python_for_example() { + for (const candidate of [process.env.PYTHON, 'python', 'python3']) { + if (!candidate) continue; + try { + execFileSync(candidate, ['-c', 'import robot'], { stdio: 'ignore' }); + return candidate; + } catch { + // no such interpreter, or one without robotframework: try the next + } + } + return null; +} + +function rebuild_example(worktree, args) { + if (!args.example) { + console.log('[example] --no-example: keeping the committed example dashboard'); + return; + } + const python = python_for_example(); + if (!python) { + // a docs build is not worth a python install locally, but in CI it means the workflow + // stopped installing one and dev would quietly serve a stale example again + if (process.env.CI) throw new Error('No python with robotframework found to rebuild the example dashboard'); + console.log('[example] no python with robotframework: keeping the committed example dashboard'); + return; + } + console.log('[example] rebuilding example/robot_dashboard.html from this worktree'); + execFileSync(python, ['scripts/example.py'], { cwd: worktree, stdio: 'inherit' }); +} + +function build_one(build, env, latest, args) { const worktree = resolve(worktreesDir, build.name.replace(/[^\w.-]/g, '_')); const outDir = resolve(distDir, build.prefix); console.log(`\n=== ${build.name} (${build.ref}) -> ${SITE_BASE}${build.prefix}`); @@ -317,6 +364,8 @@ function build_one(build, env, latest) { overlay(worktree); apply_patches(worktree, build); const sharedAssets = strip_shared_assets(worktree, build, latest); + // before copy-static.mjs, which copies example/robot_dashboard.html into docs/public + if (build.name === 'dev') rebuild_example(worktree, args); execFileSync(process.execPath, [resolve(worktree, 'scripts/docs/copy-static.mjs')], { cwd: worktree, stdio: 'inherit', @@ -377,7 +426,7 @@ function main() { restored++; continue; } - build_one(build, env, latest); + build_one(build, env, latest, args); if (cacheable) { store_in_cache(args.cacheDir, build, print); // tells CI that the cache dir changed and is worth saving again