Skip to content

ci: split notebook execution into its own workflow with cached artifact (closes #186) - #241

Merged
jonathanhhb merged 6 commits into
mainfrom
ci/execute-notebooks-artifact
Jul 9, 2026
Merged

ci: split notebook execution into its own workflow with cached artifact (closes #186)#241
jonathanhhb merged 6 commits into
mainfrom
ci/execute-notebooks-artifact

Conversation

@jonathanhhb

@jonathanhhb jonathanhhb commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Splits notebook execution into its own workflow (execute-notebooks.yml) with a source-hash-keyed cache, publishes results as a 400-day GitHub Actions artifact, and reworks build-combined-doc.yml to consume that artifact instead of re-executing notebooks itself.

Closes #186 (notebook execution on doc PRs/merges) and solves two adjacent problems as a side effect.

What this fixes

Concern Before After
Notebooks executed on doc-merge builds ❌ release-only ✅ every main push touching notebook/library source
Committed notebook outputs must be kept fresh manual convention not required — CI artifact is source of truth
Every doc build re-executes notebooks ✅ ~25 min only when source hash changes
Executed-notebook access for debugging latest-execute only every successful run, 400-day retention
Doc build wall clock in common case ~25 min ~2-3 min (download + build + concat)

Workflow shape

Execute Notebooks
  ├─ trigger: push to main touching docs/**/*.ipynb, src/**, deps, exec scripts,
  │           Makefile, or execute-notebooks.yml itself. Also workflow_dispatch.
  │           No pull_request trigger — see the top-of-file comment for rationale.
  ├─ cache-key: hashFiles(all *.ipynb, src/**, pyproject.toml,
  │                       docs/requirements.txt, docs/**/*.py, Makefile,
  │                       .github/workflows/execute-notebooks.yml)
  ├─ cache miss: make docs-executed-nbs
  ├─ gate: python docs/check_executed_nbs.py (fails on any nb error)
  ├─ manifest: writes dist/executed_nbs/manifest.json with commit_sha,
  │             source_hash, run_id, python_version, event_name,
  │             was_cache_hit, was_forced, was_allow_errors
  └─ upload artifacts:
       - `executed_nbs` (400d) — only on success from push-to-main OR
                                 clean workflow_dispatch
       - `executed_nbs-debug-<run_id>` (30d) — debug dispatch with
                                               allow_notebook_errors=true
       - `executed_nbs-failed-<run_id>` (30d) — any failure path

Build Combined Doc
  ├─ trigger: workflow_run completion of Execute Notebooks (auto-chain,
  │           restricted to push-triggered upstream runs — debug dispatches
  │           can't auto-fire a laser-mcp sync)
  │           OR manual workflow_dispatch
  ├─ download executed_nbs artifact (no re-execution)
  ├─ provenance gate (workflow_dispatch only): compare artifact's
  │   manifest.source_hash to hashFiles() of the checkout; fail if they
  │   disagree unless `use_latest_anyway=true` is set
  └─ make docs-jenner-artifact → build site + concat → sync to laser-mcp

Files changed

Policy decision: committed outputs are decorative (Option A)

Documented inline in execute-notebooks.yml. Under this proposal:

  • Committed executed-notebook outputs have NO effect on any CI-produced artifact.
  • Contributors MAY commit executed notebooks (for GitHub-render friendliness) OR strip outputs (via nbstripout) — both work identically for the doc build.
  • Repo bloat, PR-review-diff noise, and possible drift are accepted tradeoffs in exchange for zero-friction contributor workflow and inline GitHub notebook rendering.
  • If future consensus reverses this to strict source-only commits (Option B), the enforcement point is one check step in github-actions.yml. Explicitly not enforced today.

No pull_request trigger — deliberate

Executing every notebook adds ~25 min per PR iteration. That cost is not affordable during review cycles. Broken notebooks land on main and are surfaced by the post-merge push run within minutes. Local make docs-jenner-execute (available once #237 lands) or a manual workflow_dispatch on this workflow remain as pre-merge validation options for PRs that specifically need them.

Bootstrap note

On first enable, workflow_dispatch on Build Combined Doc will fail cleanly (no prior successful Execute Notebooks artifact to consume — if_no_artifact_found: fail catches this). Kick off Execute Notebooks once manually to seed. After that, either trigger works.

Third-party action

dawidd6/action-download-artifact@v3 — needed because actions/download-artifact only fetches from the current run or a known run-id, and workflow_dispatch on Build Combined Doc has no upstream run-id to point at. This third-party action searches for "latest successful run of workflow X" — exactly the semantics we need. Widely used (~4M weekly downloads); if you'd rather avoid the dep, we can write ~20 lines of gh api to find and fetch the latest artifact ID.

Merge order with in-flight PRs

Test plan

  • YAML validity confirmed on both workflow files.
  • make help shows the new target.
  • Both hashFiles lists (cache key in execute-notebooks + compat check in build-combined-doc) are literal duplicates: 7 patterns in same order.
  • After merging, kick off Execute Notebooks manually to bootstrap the first artifact.
  • Verify Build Combined Doc auto-triggers on Execute Notebooks completion.
  • Verify Build Combined Doc workflow_dispatch grabs the most recent artifact and passes the compat check.
  • Verify cache hits on a no-notebook-change push (workflow completes in seconds).
  • Verify cache miss on a notebook edit (full ~25 min re-execution).

🤖 Generated with Claude Code

Jonathan Bloedow and others added 2 commits July 8, 2026 11:50
Adds .github/workflows/execute-notebooks.yml as a source-hash-cached
notebook-execution stage, and reworks build-combined-doc.yml to consume its
"executed_nbs" artifact instead of re-executing notebooks itself.

Solves three related problems at once (see issue #186):

  1. Doc PRs and merges don't currently execute notebooks — only the
     release workflow does. This makes notebook execution a first-class CI
     stage that fires on every main push affecting notebook or library
     source.

  2. Committed executed-notebook outputs drift from committed source by
     convention only. This pattern moves executed outputs out of git
     entirely — the artifact IS the source of truth, and the source-hash
     cache key guarantees outputs are always regenerated when any input
     that could affect them changes.

  3. Every doc build re-executes notebooks (~25 min per build). Under
     this pattern the doc build downloads a fresh artifact (~seconds)
     and only does site build + concat. Execute Notebooks only re-runs
     when notebook / library / pyproject source hashes change.

## Workflow shape

  Execute Notebooks
    ├─ trigger: push touching docs/**/*.ipynb, src/**, pyproject.toml
    ├─ cache-key: hashFiles(all *.ipynb, src/**, pyproject.toml)
    ├─ on cache miss: make docs-executed-nbs
    ├─ gate: python docs/check_executed_nbs.py (fails on any nb error)
    └─ upload artifact "executed_nbs", retention: 400 days

  Build Combined Doc
    ├─ trigger: workflow_run completion of Execute Notebooks (auto-chain)
    │           OR manual workflow_dispatch (grabs latest successful)
    ├─ download executed_nbs artifact (no re-execution)
    └─ make docs-jenner-artifact → build site + concat → sync to laser-mcp

## Makefile

Adds `docs-jenner-artifact` — the CI-side counterpart to `docs-jenner`
(local: assumes committed outputs) and `docs-jenner-execute` (local:
full execute + check + build + concat). Same output as docs-jenner but
skips both docs-executed-nbs and docs-check-nbs — assumes $(EXEC_DIR) is
pre-populated by the artifact download.

## Bootstrap note

On first enable, workflow_dispatch on Build Combined Doc will fail cleanly
(no prior successful Execute Notebooks artifact to consume — the download
step's `if_no_artifact_found: fail` catches this). Kick off Execute
Notebooks once manually to seed. After that, either trigger works.

## Merge order with in-flight PRs

  - Independent of #204 (plot descriptions), #240 (figtext), #236 (concat
    improvements) — those change content; this changes CI plumbing.
  - Compatible with #237 (skip re-exec in docs-jenner) — that PR is about
    local dev; this pattern replaces CI-side reasoning about executed
    outputs entirely. Both land cleanly.

Closes #186.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…itative)

Adds a policy block to execute-notebooks.yml stating that committed
notebook outputs are informational only (GitHub-render friendliness) and
have no effect on CI-produced artifacts. Contributors may commit executed
or stripped notebooks interchangeably; both work.

Documents this so future maintainers understand the tradeoff — repo bloat
+ PR-review noise + possible drift accepted, in exchange for
browsing-on-github.com rendering and no developer friction.

If a future decision reverses this (Option B), the enforcement point is
identified inline: a single check step in github-actions.yml that fails
PRs whose notebooks contain non-empty outputs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jonathanhhb pushed a commit that referenced this pull request Jul 8, 2026
Re-executed all 18 tutorial notebooks with the figtext-injected source
(committed in 285308b) and committed the resulting outputs. Every plot
now shows the takeaway sentence as an 8pt caption below the axes,
matching what a fresh laser-generic doc build would render.

Motivation:

  - Committed notebook outputs were rendered before figtext calls were
    added, so the previously-committed figure PNGs didn't reflect the
    source. Someone browsing docs/tutorials/notebooks/*.ipynb on
    github.com would see charts without captions, while a fresh
    `make docs-jenner-execute` would render them WITH captions.
  - Under the "committed outputs are decorative" policy proposed for
    laser-generic PR #241, this doesn't affect any downstream doc/RAG
    build (those consume the Execute Notebooks CI artifact). But since
    github.com renders committed notebooks inline, keeping outputs
    fresh preserves the browsing experience.

Change shape:

  - 18 notebook files, source identical (verified byte-for-byte via a
    cell-by-cell comparison against 285308b), outputs replaced with
    freshly-executed versions.
  - EW_analysis.ipynb untouched (excluded from the docs-executed-nbs
    pass via NB_EXCLUDE).
  - Execution environment: GITHUB_ACTIONS=true, so nb06 used the
    env-var-lite path (n_years=20, nsims=2) introduced in #233 —
    matching what CI would produce.

Verified caption placement in nb01 (3 plots) and nb06 (2 plots): 8pt
figtext below the axes, wraps to ~2 lines, LaTeX ($T ≈ 2\pi\sqrt{AG}$,
Greek subscripts) renders correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonathanhhb
jonathanhhb requested review from clorton and Copilot July 9, 2026 20:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the documentation CI pipeline by moving notebook execution into a dedicated workflow that publishes executed notebooks as a long-retention artifact, and updating the combined-doc build workflow to consume that artifact rather than re-executing notebooks.

Changes:

  • Adds a new “Execute Notebooks” workflow that executes notebooks, gates on execution errors, caches results, and uploads an executed_nbs artifact.
  • Updates “Build Combined Doc” to auto-chain off the notebook-execution workflow (or run manually) and to build/concat from the downloaded artifact.
  • Adds a docs-jenner-artifact Makefile target for building/concatenating when dist/executed_nbs is already populated.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
Makefile Adds docs-jenner-artifact to build+concat from a pre-populated executed-notebooks tree.
.github/workflows/execute-notebooks.yml New workflow to execute notebooks, cache by source hash, gate on errors, and upload a long-retention artifact.
.github/workflows/build-combined-doc.yml Downloads executed_nbs artifact (per-trigger strategy) and builds the combined doc without notebook execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/execute-notebooks.yml Outdated
Comment on lines +31 to +36
- 'docs/**/*.ipynb' # notebook source changed
- 'src/**' # library that notebooks import changed
- 'pyproject.toml' # dependencies changed
- 'docs/execute_notebooks.py' # exec harness itself changed
- 'docs/check_executed_nbs.py' # error gate changed
- '.github/workflows/execute-notebooks.yml'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — addressed in 4fa876f. Added docs/requirements.txt and Makefile to the paths filter.

Comment on lines +78 to +80
path: dist/executed_nbs
key: executed-nbs-v1-${{ hashFiles('docs/**/*.ipynb', 'src/**/*.py', 'pyproject.toml') }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4fa876f. Added docs/requirements.txt, docs/execute_notebooks.py, and Makefile to the cache-key hashFiles list and bumped the key prefix v1 -> v2 to invalidate historic pool entries built under the narrower key.

Comment on lines +19 to +21
# Skip if the upstream Execute Notebooks run failed (no artifact worth
# consuming). workflow_dispatch triggers pass this check trivially.
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Fixed in 4fa876f: restricted the workflow_run auto-chain to github.event.workflow_run.event == 'push', so upstream workflow_dispatch runs (debug dispatches with allow_notebook_errors=true etc.) no longer auto-fire this workflow. Manual doc rebuilds still work via this workflow's own workflow_dispatch trigger.

@jonathanhhb
jonathanhhb marked this pull request as ready for review July 9, 2026 21:44
Jonathan Bloedow and others added 2 commits July 9, 2026 14:47
Three related fixes covering staleness and safety:

1. Path filter (execute-notebooks.yml): add docs/requirements.txt and
   Makefile. Changes to either can affect executed-notebook outputs
   (via docs-install picking up new deps, or Makefile NB_EXCLUDE /
   NB_TIMEOUT changes) but previously wouldn't trigger the workflow.

2. Cache key (execute-notebooks.yml): add docs/requirements.txt,
   docs/execute_notebooks.py, and Makefile to the hashFiles set. Without
   these, editing (e.g.) the executor script would trigger the workflow
   via the paths filter but still hit an old cache entry, producing
   outputs from the pre-change executor. Bumped key prefix v1 -> v2 to
   invalidate any historic pool built under the narrower key.

3. workflow_run auto-chain (build-combined-doc.yml): restrict to
   push-triggered upstream Execute Notebooks runs. A debug dispatch of
   Execute Notebooks (allow_notebook_errors=true or force_reexecute=true)
   would previously auto-fire Build Combined Doc and push a
   possibly-corrupted corpus PR into laser-mcp. Manual doc rebuilds
   continue to work via this workflow's own workflow_dispatch trigger.

Also added a top-of-file comment on the path filter making the
"kept in sync with the cache-key hashFiles below" contract explicit,
so future edits to either side are more likely to update both.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five changes tightening the artifact contract, per detailed review:

1. PR validation trigger. Adds pull_request to execute-notebooks.yml with
   the same paths filter. PR runs execute notebooks and run the error
   gate — reviewers see breakage before merge, not after. PR-scoped
   artifact name (executed_nbs-pr-<N>) prevents accidental pollution of
   the canonical artifact stream.

2. Artifact manifest. execute-notebooks now writes
   dist/executed_nbs/manifest.json before upload, containing:
     schema_version, commit_sha, commit_ref, source_hash, python_version,
     run_id, run_number, workflow_url, event_name, created_at,
     was_cache_hit, was_forced, was_allow_errors.
   Consumers can verify provenance and detect drift.

3. Cache-key correctness. Extended hashFiles set to include docs/**/*.py
   (defensive against future doc scripts) beyond the docs/requirements.txt
   / Makefile additions from the prior fix. Source hash is now computed
   ONCE via a `Compute source hash` step (id: source-hash) and reused in
   both the cache key and the manifest — no chance of the two drifting.
   Bumped key prefix v2 -> v3 for the new hash set.

4. Non-canonical upload on failure/debug. Split what used to be a single
   `if: always()` upload of `executed_nbs` into four conditional uploads
   under distinct names, so Build Combined Doc's "latest successful"
   lookup can never match a poisoned artifact:
     canonical `executed_nbs` (400d)  <- push-to-main OR clean dispatch
     `executed_nbs-pr-<N>` (90d)      <- pull_request success
     `executed_nbs-debug-<run>` (30d) <- dispatch w/ allow_notebook_errors
     `executed_nbs-failed-<run>` (30d) <- any failure path

5. Compatibility gate on manual dispatch (build-combined-doc.yml).
   workflow_dispatch now computes hashFiles of the checked-out tree and
   compares against the artifact's manifest.source_hash. Mismatch fails
   the build with an actionable error message. New workflow input
   `use_latest_anyway` (default false) bypasses the check for cases where
   building against an older artifact is intentional. workflow_run path
   is unaffected — it already fetches by explicit run-id.

Kept in sync: the paths filter (pull_request + push) and the source-hash
step's hashFiles list are literal duplicates by design. Comment above
Compute source hash notes that all three lists must move together.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonathanhhb

Copy link
Copy Markdown
Collaborator Author

Addressed the 5 guardrails in c5884cb

Concise mapping of the review's concerns to what changed:

# Concern Fix
1 PR validation gap Added pull_request trigger to execute-notebooks.yml with the same paths filter. PR runs execute notebooks + run the error gate. Uploads to executed_nbs-pr-<N> (distinct name) so it can't be picked up by Build Combined Doc's "latest successful" lookup. No laser-mcp sync side effects — that lives in build-combined-doc.yml, unchanged for PR runs.
2 Artifact provenance Writes dist/executed_nbs/manifest.json before upload with schema_version, commit_sha, commit_ref, source_hash, python_version, run_id, run_number, workflow_url, event_name, created_at, was_cache_hit, was_forced, was_allow_errors. Placed inside the artifact so download-artifact includes it automatically.
3 Cache correctness Extended hashFiles set: added docs/**/*.py (catches future doc scripts). No mkdocs.yml because it's only used by docs-build after execution (mkdocs-jupyter runs with execute=false). No lockfile because none exists in the repo today. Refactored to compute the hash ONCE in a Compute source hash step (id: source-hash) — the cache key AND the manifest read the same output, so they can't drift. Bumped key prefix v2 -> v3 for the new hash set.
4 if: always() upload risky Split into four conditional uploads under distinct names: executed_nbs (canonical, push-to-main OR clean dispatch, 400d), executed_nbs-pr-<N> (90d), executed_nbs-debug-<run_id> (dispatch with allow_notebook_errors=true, 30d), executed_nbs-failed-<run_id> (any failure, 30d). Build Combined Doc's "latest successful" lookup can only ever match the canonical name.
5 Manual dispatch compat check New step in build-combined-doc.yml: when triggered by workflow_dispatch, computes hashFiles of the checkout with the same file set, compares to manifest.source_hash, fails on mismatch with an actionable error. New input use_latest_anyway (default false) bypasses the check for deliberate stale-artifact builds. workflow_run path unaffected — it already fetches by explicit run-id, no drift possible.

The three lists that must stay in sync (paths filter × 2 triggers + hashFiles) are literal duplicates by design; comment on the Compute source hash step calls this out for future editors.

CI checks pending on the new tip. Ready for another look.

Undoes point 1 of the earlier 5-guardrail commit. Executing every notebook
adds ~25 min of wall-clock per PR iteration, which is not affordable during
review cycles. The alpha suite already exercises the RAG corpus end-to-end,
and post-merge push runs surface any notebook breakage within minutes of
landing on main.

Kept the top-of-file comment explicitly stating this is a deliberate choice,
so a future reviewer doesn't re-add the trigger without knowing the cost
tradeoff. Local `make docs-jenner-execute` and manual workflow_dispatch
remain as pre-merge validation options for PRs that specifically need them.

Also removed the now-orphan `executed_nbs-pr-<N>` upload step. The
remaining four artifact names still cleanly separate canonical from debug
paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonathanhhb

Copy link
Copy Markdown
Collaborator Author

Reverted guardrail #1 (pull_request trigger) in 760bc64

On reflection: adding ~25 min of wall-clock per PR iteration is not affordable during review cycles, regardless of the correctness win. Removed the pull_request trigger and the associated executed_nbs-pr-<N> upload step.

Notebook execution is now:

  • Automatic: post-merge push to main touching .ipynb / library source / deps / Makefile / exec scripts.
  • Manual: workflow_dispatch on this workflow (with force_reexecute if needed).
  • Local: make docs-jenner-execute for pre-merge validation on a specific PR.

Broken notebooks land on main and are surfaced by the post-merge push run within minutes. The alpha suite still exercises the resulting RAG corpus end-to-end. Reduced pre-merge safety accepted in exchange for fast PR cycles.

Left the top-of-file comment explicitly stating this is a deliberate choice, so a future reviewer doesn't re-add the trigger without seeing the cost tradeoff. The other four guardrails (manifest.json, extended cache key, non-canonical failure/debug uploads, compatibility check on manual dispatch) are unchanged.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/execute-notebooks.yml
Comment thread .github/workflows/build-combined-doc.yml
Comment thread Makefile
Comment on lines 34 to +37
@echo " make docs-jenner Full pipeline (execute + check + build + concat)"
@echo " Output: $(COMBINED)"
@echo " make docs-jenner-artifact Build + concat only (assumes \$$(EXEC_DIR) pre-populated)"
@echo " Used by the Execute Notebooks -> Build Combined Doc CI chain"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — the 'trio' framing in the PR description was aspirational: it only applies after #237 also lands (that PR is what renames the current docs-jenner into docs-jenner-execute and adds the fast docs-jenner). This PR alone only adds docs-jenner-artifact. Fixing the PR description to reflect that, rather than stepping on #237's Makefile changes here.

Two related fixes:

1. execute-notebooks.yml cache key: add
   .github/workflows/execute-notebooks.yml to the hashFiles set. The
   workflow file is already in the push paths filter, but was missing
   from the cache key — meaning a change to (e.g.) python-version, env
   vars, or command semantics in the workflow would trigger a run but
   hit the old cache entry and skip re-execution. Bumped cache key
   prefix v3 -> v4 to invalidate historic pool.

2. build-combined-doc.yml compat check: mirror the same addition in the
   provenance-hash comparison, so the workflow_dispatch mismatch check
   stays aligned with what execute-notebooks.yml actually hashes. The
   two hashFiles lists must be identical or the check produces false
   positives/negatives.

Both lists are now literal duplicates by design — 7 patterns each,
same order.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonathanhhb
jonathanhhb merged commit 987ede0 into main Jul 9, 2026
8 checks passed
jonathanhhb added a commit that referenced this pull request Jul 9, 2026
…vars (#244)

The manifest-write step in Execute Notebooks failed on its first
post-#241 push run (29054294479) with `NameError: name 'false' is not
defined`. The heredoc substituted `\${{ ... }}` expressions directly
into Python source, but GHA renders booleans as lowercase `true`/`false`
— invalid Python identifiers.

Fix: pass every substitution through an `env:` block, read via
`os.environ` in Python, and route booleans through an `as_bool()` helper
that compares against the string `"true"`. Removes the
substitution-into-source hazard entirely — no future field addition can
silently break the surrounding Python.

Verified locally by running the step's Python code with representative
env values → produces a valid manifest.json.

Design safety confirmed on the failed run: only the non-canonical
`executed_nbs-failed-<run_id>` artifact uploaded (via `if: failure()`),
canonical `executed_nbs` upload correctly skipped, Build Combined Doc's
workflow_run trigger saw `conclusion: failure` and skipped its job — no
half-baked artifact went downstream to laser-mcp.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jonathanhhb added a commit that referenced this pull request Jul 9, 2026
)

Follow-up to #241. Now that the Execute Notebooks -> Build Combined Doc
chain is verified working end-to-end, makes the executed-notebooks
artifact discoverable from two places without touching CI logic.

## Changes

- **`README.md`**
  - New "Execute Notebooks" status badge alongside the existing build /
    coverage badges.
  - New "Executed notebook artifacts" section explaining what the
    artifact is, the 3-step download recipe, and pointing at
    `manifest.json` for provenance.
- **`docs/tutorials/notebooks/README.md`** (new file)
  - Folder-local pointer to the same artifact download flow.
  - Clarifies the "committed outputs are decorative, source is
    authoritative" policy so contributors don't fight committed-output
    drift.
  - Documents the `manifest.json` provenance fields (commit_sha,
    source_hash, python_version, run_id, was_cache_hit, etc.).
  - Shows local regeneration commands including the `GITHUB_ACTIONS=true`
    toggle for nb06's lite path.

Renders on github.com — anyone browsing `docs/tutorials/notebooks/` sees
the same guidance a docs-site reader would.

## Verified working chain

The pointers reference the actual working artifact pipeline that was
verified in a full end-to-end pass after #241 landed:
- Execute Notebooks run 29056479852 (cache miss, ~30 min): produced
  `executed_nbs` (4.48 MB canonical, `manifest.json` inside).
- Execute Notebooks run 29057994100 (cache hit, ~3 min): confirmed the
  ~10x speedup on unchanged source.
- Build Combined Doc run 29057992879: downloaded artifact, passed the
  provenance-check gate, built the combined doc, opened / updated the
  laser-mcp sync PR (#38 in laser-mcp).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jonathanhhb pushed a commit that referenced this pull request Jul 10, 2026
The docs site at laser.idmod.org/laser-generic was being deployed by
mkdocs-ghp.yml with `mkdocs-jupyter execute: false`, which meant it
rendered whatever notebook `outputs` were committed in docs/tutorials/
notebooks/*.ipynb. Under the "committed outputs are decorative" policy
adopted in #241, those outputs may drift from source over time — so
the docs site could show stale figures while the RAG corpus (fed by
the executed_nbs artifact via Build Combined Doc) had fresh ones.

This inconsistency was called out during review of #245: the docs
site is supposed to be the fresh, user-facing view.

Fix: before `mkdocs build`, overlay the latest successful executed_nbs
artifact onto the checked-out docs/ tree. Two download paths mirror
Build Combined Doc's pattern:
  - workflow_run trigger: fetch the specific triggering run's artifact
    by run-id. Restricted to push-triggered upstream runs so debug
    dispatches don't auto-publish a suspect site.
  - push / workflow_dispatch trigger: fetch the latest successful
    Execute Notebooks artifact (may lag current commit if the push
    doesn't affect notebook execution — that's fine; a following
    workflow_run chain rebuilds when execution finishes).

Bootstrap-safe: if_no_artifact_found: warn on the fallback path, so
the very first deploy after enabling this (before any Execute
Notebooks run has completed) still succeeds using committed
decorative outputs. Emits a clear warning in the Actions log.

The concurrency group (gh-pages-deploy, cancel-in-progress) already
handles overlap: a push-triggered deploy that starts before Execute
Notebooks completes will be superseded by the workflow_run chain
firing with the fresher artifact.

After this lands, the "docs site" row of the source-of-truth table in
PR #245 becomes accurate: fresh, matching the RAG corpus, always
rebuilt from CI-executed outputs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jonathanhhb added a commit that referenced this pull request Jul 10, 2026
The docs site at laser.idmod.org/laser-generic was being built with
mkdocs-jupyter's `execute: false`, so it rendered whatever notebook
`outputs` were committed under docs/tutorials/notebooks/*.ipynb. Under
the "committed outputs are decorative" policy adopted in #241, those
outputs may drift from source — so the docs site could show stale
figures while the RAG corpus (fed by the executed_nbs artifact via
Build Combined Doc) had fresh ones.

Fix: before `mkdocs build`, download the latest successful executed_nbs
artifact and overlay its `.ipynb` files onto the checked-out docs/
tree. Two download paths mirror Build Combined Doc:

  - workflow_run trigger: fetch by run-id from the triggering upstream
    Execute Notebooks run. Restricted to push-triggered, successful
    upstream runs so debug dispatches don't auto-publish a suspect site.
  - push / workflow_dispatch trigger: fetch latest successful via
    dawidd6/action-download-artifact@v6. `if_no_artifact_found: warn`
    on this path — bootstrap-safe: first-time-ever deploy still succeeds
    with committed decorative outputs and emits a clear warning.

Permissions block explicitly grants `actions: read` alongside the
existing `contents: write`. Both download-artifact steps need
`actions: read` to reach OTHER workflow runs' artifacts; the previous
implicit-default worked when no permissions block was set, but the
existing explicit `contents: write` overrides defaults, so
`actions: read` had to be added or downloads would 403 at runtime.

The concurrency group (gh-pages-deploy, cancel-in-progress: true)
already handles the overlap case: a push-triggered deploy that starts
before Execute Notebooks completes will be superseded by the
workflow_run chain firing with the fresher artifact.

Validated locally (Option A test): downloaded the artifact from run
29057994100, applied the same overlay logic on a scratch worktree at
pristine main, ran mkdocs build. nb06's rendered page contained the
CI-executed "Regime robustness" figure (only exists if the notebook
was actually executed) among six real plot figures.

After this lands, the "docs site" row of the source-of-truth policy
becomes accurate:
  docs site        <- Execute Notebooks artifact (was: committed outputs)
  RAG corpus       <- Execute Notebooks artifact (unchanged)
  committed .ipynb <- committed decorative outputs (Option A, unchanged)

Enables PR #245 (README reframe) to make the docs-site freshness claim
truthfully.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update GHA for doc PRs and merges to execute notebooks

2 participants