Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: 2

updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
cooldown:
default-days: 3

# Development tools only. Runtime packages are refreshed with
# `make upgrade-runtime-deps`, because production vendors pylock.toml and
# their versions depend on which Pyodide wheels exist. Ruff upgrades stay
# deliberate: new releases add lint rules and must match `required-version`.
- package-ecosystem: uv
directory: /
schedule:
interval: weekly
cooldown:
default-days: 3
allow:
- dependency-name: hypothesis
- dependency-name: pillow
- dependency-name: workers-py

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 3
groups:
github-actions:
patterns:
- "*"
11 changes: 7 additions & 4 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: false
- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: '3.13'
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: '22'
package-manager-cache: false
- name: Install locked dependencies
run: |
uv sync --locked --all-groups
Expand All @@ -51,6 +52,8 @@ jobs:
CHROME_PATH: /usr/bin/google-chrome
run: |
npm audit --audit-level=high
# pywrangler dev re-resolved pylock.toml above; production must match the commit.
git diff --exit-code -- pylock.toml
make verify
scripts/format_examples.py --check
make verify-python-version VERSION=3.13
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0

### Fixed

- Production Python packages are locked. Pywrangler 1.17.4 vendors a committed, hash-pinned `pylock.toml` instead of resolving an unpinned `fastapi` at deploy time, and `uv.lock` pins the same versions (FastAPI 0.141.1, Starlette 1.7.0, Pydantic 2.10.6) so the test suite runs against exactly what ships. `tests/test_dependency_locks.py` fails on drift, and CI and `make deploy` fail if a sync would change `pylock.toml`. Starlette 1.7.0 also clears the five advisories against the previously tested 1.0.0.
- Example-page runner wiring (Run interception, Reset, the share button, and keyboard navigation) no longer waits for the CDN-backed highlighter and editor modules: `runner.js` loads `async`, so a slow or unreachable esm.sh cannot stall it — ordered module scripts otherwise execute strictly after every preceding module settles, including their top-level awaits.

### Added
Expand All @@ -32,6 +33,9 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0

### Changed

- Dependency refresh: Pillow 12.3.0 (from 11.3.0; 17 advisories), Pywrangler 1.17.4 (from 1.9.3; requires uv 0.12.3+), Shiki 4.4.3 (from 1.29.2; byte-identical output for every example block in both themes), and CodeMirror state 6.7.6, view 6.43.13, and language 6.12.4.
- CI actions moved to their Node 24 releases (checkout, setup-node, and setup-python v7; setup-uv v10.2.0 pinned by commit), clearing the Node.js 20 deprecation warning; npm caching stays off.
- Dependabot proposes weekly npm, GitHub Actions, and development-tool (Hypothesis, Pillow, Pywrangler) updates. Runtime packages refresh through `make upgrade-runtime-deps`.
- The home header stays visible on landing: the scroll-driven entrance now animates the brand and a compositor-friendly veil layer instead of hiding the whole header (which left the page with no visible nav at first paint).
- Body type is `100%/1.6` instead of a fixed 16px, so browser text-size settings are respected. The never-rendering brand faces (FT Kunst Grotesk, Apercu Mono Pro — installed nowhere, shipped never) are removed from the stacks, which now declare the real design: `system-ui` prose and `ui-monospace`-first code. The `/about` type specimen says so.
- `prefers-reduced-transparency` and `prefers-contrast: more` get solid-chrome and defined-border fallbacks; cards press down on tap; header links carry full-height (~43px) tap targets via padding with cancelling negative margins.
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
PY := uv run --python 3.13
NODE_DEPS_STAMP := node_modules/.package-lock.json

.PHONY: check-node-version node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint
.PHONY: check-node-version node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy upgrade-runtime-deps lint

check-node-version:
@major="$$(node -p 'process.versions.node.split(".")[0]')"; \
Expand Down Expand Up @@ -125,4 +125,10 @@ smoke-deployment:

deploy: node-deps check-generated
uv run --group workers pywrangler sync --force
git diff --exit-code -- pylock.toml
uv run --group workers pywrangler deploy

# Production vendors pylock.toml; tests run against uv.lock. Refresh both together.
upgrade-runtime-deps: node-deps
uv run --group workers pywrangler sync --force --upgrade
$(PY) scripts/align_runtime_lock.py
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Python documentation links point to the official [Python documentation](https://
- `src/examples.py` is a compatibility shim for the loaded catalog.
- `src/example_sources_data.py` is generated embedded source data for Cloudflare Workers.
- `public/` contains static assets served by Workers Assets.
- `pylock.toml` is the Pyodide-targeted lock Pywrangler vendors into production; `uv.lock` pins the same runtime versions for the test suite.
- `python_modules/` is generated by the Workers tooling and intentionally ignored by Git.

Example runs are executed by Dynamic Workers. The parent Worker creates a Dynamic Worker module from the submitted code, disables outbound network access, and keys Worker Loader reuse by Python version, example slug, and code hash.
Expand All @@ -68,7 +69,7 @@ This project is developed with red-green-refactor TDD:
2. Implement the smallest change that makes it pass.
3. Refactor while keeping tests green.

Use Node 22 (Pywrangler's Pyodide runtime is not compatible with Node 26), then install the exact committed Python and Node dependency sets:
Use Node 22 (Pywrangler's Pyodide runtime is not compatible with Node 26) and uv 0.12.3 or newer (Pywrangler's minimum), then install the exact committed Python and Node dependency sets:

```bash
node --version # v22.x
Expand Down Expand Up @@ -173,7 +174,18 @@ scripts/format_examples.py --check
make deploy
```

`make deploy` first runs `make check-generated`, which rebuilds and rejects any generated output not committed to the branch. It then syncs the ignored Python Workers dependency bundle before Wrangler deploys.
`make deploy` first runs `make check-generated`, which rebuilds and rejects any generated output not committed to the branch. It then syncs the ignored Python Workers dependency bundle from the committed `pylock.toml`, refusing to deploy if the sync would change that lock, before Wrangler deploys.

## Updating dependencies

Production Python packages come from `pylock.toml`, which Pywrangler resolves against Pyodide wheels; the tests run against `uv.lock`, and `tests/test_dependency_locks.py` fails if the two disagree on any shared package. Refresh both together:

```bash
make upgrade-runtime-deps
make verify
```

Dependabot proposes npm, GitHub Actions, and development-tool updates weekly. It skips runtime packages (their versions depend on which Pyodide wheels exist) and Ruff (upgrades are deliberate because they change lint rules). The browser libraries loaded from esm.sh are pinned in `public/editor.js` and `public/syntax-highlight.js` and are updated by hand.

## Contributing

Expand Down Expand Up @@ -234,5 +246,5 @@ Use the active Cloudflare-supported Python version.
- Dynamic Workers run with `globalOutbound: null` and tight CPU/subrequest limits.
- POST example runs carry `Cache-Control: no-store` and are never cached.
- Wrangler is an exact dev dependency in `package-lock.json`; `make dev` and `make deploy` require Node 22 and install/use that local version through Pywrangler.
- `make deploy` forces a fresh Pywrangler vendor sync so an empty or stale `python_modules/` directory cannot produce a dependency-free upload.
- `make deploy` forces a fresh Pywrangler vendor sync so an empty or stale `python_modules/` directory cannot produce a dependency-free upload, and fails if that sync would change the committed `pylock.toml`.
- Production still requires the account-level WAF/rate-limit rule documented in `docs/turnstile-runner-protection-spec.md`; repository settings alone do not prove that external control is active.
3 changes: 2 additions & 1 deletion docs/lessons-learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ This document records project lessons that should guide future changes to Python
- POST runs must never go through the rendered-page cache path.
- For Cloudflare request geography, prefer `request.cf.colo` / `request.cf.country` when available. Do not assume the `cf-ray` header includes a colo suffix; `wrangler tail` has shown Ray IDs without `-SJC`-style suffixes.
- Structured application logs can be privacy-safe while the Cloudflare log envelope is not. `wrangler tail` wraps `console.log` payloads with the raw request URL and headers, including `cf-connecting-ip`, even when invocation logs are disabled. Validate sink-level privacy separately from custom-payload privacy.
- If `pywrangler deploy` fails fetching Pyodide packages with `invalid peer certificate: UnknownIssuer`, rerun with `UV_NATIVE_TLS=true` so `uv` uses system certificate roots.
- If `pywrangler deploy` fails fetching Pyodide packages with `invalid peer certificate: UnknownIssuer`, rerun with `UV_SYSTEM_CERTS=true` (formerly `UV_NATIVE_TLS`, which uv 0.12 deprecates) so `uv` uses system certificate roots.
- A lockfile only protects what installs from it. Pywrangler 1.9.3 vendored `[project] dependencies` by resolving an unpinned `fastapi` afresh at every deploy, so CI tested one FastAPI/Starlette/Pydantic set (`uv.lock`) while production shipped whatever was newest that day. Pywrangler 1.17 vendors a committed, Pyodide-targeted `pylock.toml`; keep `uv.lock` pinned to the same versions (`scripts/align_runtime_lock.py`) and let `tests/test_dependency_locks.py` fail on drift. Pyodide wheels lag PyPI, so production can legitimately run an older package (Pydantic 2.10.6) than a host-only resolve would pick.

## Cache busting and Worker Cache API

Expand Down
10 changes: 5 additions & 5 deletions public/editor.3827f6497b54.js → public/editor.3659b44c8480.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Compartment, EditorState } from 'https://esm.sh/@codemirror/state@6.5.2';
import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2';
import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1';
import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3';
import { oneDarkHighlightStyle } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3';
import { Compartment, EditorState } from 'https://esm.sh/@codemirror/state@6.7.6';
import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.43.13?deps=@codemirror/state@6.7.6';
import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.4?deps=@codemirror/state@6.7.6,@codemirror/view@6.43.13';
import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.7.6,@codemirror/view@6.43.13,@codemirror/language@6.12.4';
import { oneDarkHighlightStyle } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3?deps=@codemirror/state@6.7.6,@codemirror/view@6.43.13,@codemirror/language@6.12.4';

const themePreference = window.matchMedia('(prefers-color-scheme: dark)');
const highlightStyle = () => themePreference.matches ? oneDarkHighlightStyle : defaultHighlightStyle;
Expand Down
10 changes: 5 additions & 5 deletions public/editor.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Compartment, EditorState } from 'https://esm.sh/@codemirror/state@6.5.2';
import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2';
import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1';
import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3';
import { oneDarkHighlightStyle } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3';
import { Compartment, EditorState } from 'https://esm.sh/@codemirror/state@6.7.6';
import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.43.13?deps=@codemirror/state@6.7.6';
import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.4?deps=@codemirror/state@6.7.6,@codemirror/view@6.43.13';
import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.7.6,@codemirror/view@6.43.13,@codemirror/language@6.12.4';
import { oneDarkHighlightStyle } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3?deps=@codemirror/state@6.7.6,@codemirror/view@6.43.13,@codemirror/language@6.12.4';

const themePreference = window.matchMedia('(prefers-color-scheme: dark)');
const highlightStyle = () => themePreference.matches ? oneDarkHighlightStyle : defaultHighlightStyle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const sourceBlocks = [...document.querySelectorAll('pre code.language-python')].
if (sourceBlocks.length) {
let codeToHtml = null;
try {
({ codeToHtml } = await import('https://esm.sh/shiki@1.29.2'));
({ codeToHtml } = await import('https://esm.sh/shiki@4.4.3'));
} catch (_) {
// Plain server-rendered code remains readable when the optional CDN fails.
}
Expand Down
2 changes: 1 addition & 1 deletion public/syntax-highlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const sourceBlocks = [...document.querySelectorAll('pre code.language-python')].
if (sourceBlocks.length) {
let codeToHtml = null;
try {
({ codeToHtml } = await import('https://esm.sh/shiki@1.29.2'));
({ codeToHtml } = await import('https://esm.sh/shiki@4.4.3'));
} catch (_) {
// Plain server-rendered code remains readable when the optional CDN fails.
}
Expand Down
61 changes: 61 additions & 0 deletions pylock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
lock-version = "1.0"
created-by = "uv"
requires-python = ">=3.13.2"

[[packages]]
name = "annotated-doc"
version = "0.0.5"
wheels = [{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", upload-time = 2026-07-28T13:50:57Z, size = 5302, hashes = { sha256 = "117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101" } }]

[[packages]]
name = "annotated-types"
version = "0.8.0"
wheels = [{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", upload-time = 2026-07-23T20:16:12Z, size = 13427, hashes = { sha256 = "f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0" } }]

[[packages]]
name = "anyio"
version = "4.15.1"
wheels = [{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", upload-time = 2026-09-05T10:42:37Z, size = 132079, hashes = { sha256 = "6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101" } }]

[[packages]]
name = "fastapi"
version = "0.141.1"
wheels = [{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", upload-time = 2026-07-29T17:18:04Z, size = 131954, hashes = { sha256 = "bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3" } }]

[[packages]]
name = "idna"
version = "3.20"
wheels = [{ url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", upload-time = 2026-09-17T14:11:03Z, size = 69583, hashes = { sha256 = "ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c" } }]

[[packages]]
name = "pydantic"
version = "2.10.6"
wheels = [
{ url = "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/pydantic-2.10.6-py3-none-any.whl", hashes = { sha256 = "6773062f6635c42f052e4e836edd4ff433c0718b2dee6714d684786e59b756eb" } },
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hashes = { sha256 = "427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584" } },
]

[[packages]]
name = "pydantic-core"
version = "2.27.2"
wheels = [{ url = "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/pydantic_core-2.27.2-cp313-cp313-pyodide_2025_0_wasm32.whl", hashes = { sha256 = "3695112ef99b222e48e0a2837b055ec818b43ff5e2e0802e99f5d6d2dd36af7b" } }]

[[packages]]
name = "starlette"
version = "1.7.0"
wheels = [{ url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", upload-time = 2026-09-23T07:30:24Z, size = 78980, hashes = { sha256 = "67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e" } }]

[[packages]]
name = "typing-extensions"
version = "4.16.0"
wheels = [{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", upload-time = 2026-07-02T08:40:04Z, size = 45571, hashes = { sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" } }]

[[packages]]
name = "typing-inspection"
version = "0.4.4"
wheels = [{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", upload-time = 2026-08-12T12:37:24Z, size = 14750, hashes = { sha256 = "65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" } }]

[[packages]]
name = "workers-runtime-sdk"
version = "1.9.0"
wheels = [{ url = "https://files.pythonhosted.org/packages/94/3d/053deb135d4db2ef6f6c3ccdeb86d7829de3c271f7ae02cec73da8f65891/workers_runtime_sdk-1.9.0-py3-none-any.whl", upload-time = 2026-09-18T21:08:37Z, size = 38585, hashes = { sha256 = "36209f94984686264db4e1fd410d51ac3460f2a2ab3cc29869699b975e1e1331" } }]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ workers = [
]
dev = [
"hypothesis>=6.0",
"pillow>=11.3,<12",
"pillow>=12.3,<13",
"ruff==0.16.0",
]

Expand Down
Loading
Loading