diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index de1d504e..9f7e78cb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,6 +41,26 @@ jobs: - name: Install dependencies run: uv sync --extra docs + # Inject the public Supabase anon key so the savings leaderboard works on + # the published docs site. Missing/empty (e.g. fork PRs) leaves the + # leaderboard gracefully disabled. The key is read from env (not inlined) + # and JSON-encoded into a JS string literal to avoid any injection. + - name: Inject leaderboard Supabase anon key + env: + OPENJARVIS_LEADERBOARD_ANON: ${{ secrets.VITE_SUPABASE_ANON_KEY }} + run: | + python3 - <<'PY' + import json, os, pathlib + + key = os.environ.get("OPENJARVIS_LEADERBOARD_ANON", "") + pathlib.Path("docs/javascripts/leaderboard-config.js").write_text( + "// Generated at docs-build time from the VITE_SUPABASE_ANON_KEY secret.\n" + "window.OPENJARVIS_SUPABASE_ANON_KEY = " + json.dumps(key) + ";\n", + encoding="utf-8", + ) + print("leaderboard anon key:", "set" if key else "empty (leaderboard disabled)") + PY + - name: Build documentation run: uv run mkdocs build diff --git a/docs/javascripts/leaderboard-config.js b/docs/javascripts/leaderboard-config.js new file mode 100644 index 00000000..9f88a889 --- /dev/null +++ b/docs/javascripts/leaderboard-config.js @@ -0,0 +1,12 @@ +// Public Supabase config for the savings leaderboard. +// +// This file is loaded *before* leaderboard.js and supplies the anon key it +// reads from `window.OPENJARVIS_SUPABASE_ANON_KEY`. The key is injected at +// docs-build time from the VITE_SUPABASE_ANON_KEY repo secret (see +// .github/workflows/docs.yml). It is intentionally empty here so that local +// `mkdocs build` and fork pull requests — which have no secret — render the +// graceful "Leaderboard not configured yet" message instead of failing. +// +// The anon key is public by design: Supabase Row-Level Security protects the +// data, so shipping it in the public docs bundle is expected. +window.OPENJARVIS_SUPABASE_ANON_KEY = ""; diff --git a/mkdocs.yml b/mkdocs.yml index 7cd82f57..228056dc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -127,6 +127,7 @@ markdown_extensions: - pymdownx.tilde extra_javascript: + - javascripts/leaderboard-config.js - javascripts/leaderboard.js - https://cdn.jsdelivr.net/npm/@docsearch/js@3 - javascripts/docsearch-init.js diff --git a/tests/deployment/test_docs_leaderboard.py b/tests/deployment/test_docs_leaderboard.py new file mode 100644 index 00000000..911a68b5 --- /dev/null +++ b/tests/deployment/test_docs_leaderboard.py @@ -0,0 +1,45 @@ +"""Static guards for the docs-site savings-leaderboard Supabase wiring. + +`docs/javascripts/leaderboard.js` reads the public Supabase anon key from +`window.OPENJARVIS_SUPABASE_ANON_KEY`. That global is set by a generated +config file (`leaderboard-config.js`) which must load *before* leaderboard.js, +and whose value is injected at docs-build time from the VITE_SUPABASE_ANON_KEY +secret (see `.github/workflows/docs.yml`). These are text-only checks — no +mkdocs build required — so they run in the default CI lane. +""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +MKDOCS = ROOT / "mkdocs.yml" +DOCS_WORKFLOW = ROOT / ".github" / "workflows" / "docs.yml" +CONFIG_JS = ROOT / "docs" / "javascripts" / "leaderboard-config.js" +LEADERBOARD_JS = ROOT / "docs" / "javascripts" / "leaderboard.js" + +_ANON_GLOBAL = "window.OPENJARVIS_SUPABASE_ANON_KEY" + + +def test_config_js_declares_anon_key_global(): + assert CONFIG_JS.is_file(), "leaderboard-config.js is missing" + assert _ANON_GLOBAL in CONFIG_JS.read_text() + + +def test_leaderboard_reads_the_anon_key_global(): + # leaderboard.js must consume the global the config file sets. + assert _ANON_GLOBAL in LEADERBOARD_JS.read_text() + + +def test_config_is_loaded_before_leaderboard_in_mkdocs(): + content = MKDOCS.read_text() + cfg = content.index("javascripts/leaderboard-config.js") + lb = content.index("javascripts/leaderboard.js") + assert cfg < lb, "leaderboard-config.js must be listed before leaderboard.js" + + +def test_docs_workflow_injects_the_anon_key(): + content = DOCS_WORKFLOW.read_text() + assert "VITE_SUPABASE_ANON_KEY" in content, "workflow doesn't read the secret" + assert "leaderboard-config.js" in content, "workflow doesn't write the config file" + assert _ANON_GLOBAL in content, "workflow doesn't set the anon-key global"