fix(app): the masthead has been claiming v1.0 since the CSP rollout - #10485
Conversation
The version beside `~/anyplot.ai` is read from the GitHub releases API by `useLatestRelease`. The Content-Security-Policy added in the 2026-07-16 security-headers audit never listed `api.github.com` under `connect-src`, so every browser blocked that request, the hook resolved to `null`, and the masthead rendered its hardcoded `'v1.0'` fallback. v3.0.0 and v3.1.0 both shipped to a site advertising v1.0; returning visitors kept seeing a plausible number from their localStorage cache, which hid it for two releases. Allow `api.github.com` in `connect-src`, and stop the fallback from inventing a number: Vite now injects the `[project]` version from `pyproject.toml` — the field the release flow bumps — as `__APP_VERSION__`, and the masthead falls back to that. It matters beyond this bug, because the fallback is on screen during every cold load and stays there whenever GitHub rate-limits a visitor's IP. `CONFIG.appVersion` switches from `app/package.json` (private npm metadata, stale at 2.0.0 through the 3.x line) to the injected value, and a test asserts the injected version matches `pyproject.toml` so the two cannot drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hw82Vmq7yUMk8YoXTfSzBV
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hw82Vmq7yUMk8YoXTfSzBV
There was a problem hiding this comment.
Pull request overview
This PR fixes the frontend masthead version display by ensuring the GitHub Releases API request is permitted by the site CSP and by replacing the previously hardcoded v1.0 fallback with a build-time version sourced from the repo’s single release authority (pyproject.toml).
Changes:
- Allow
https://api.github.cominconnect-srcsouseLatestReleasecan fetch release tags under the CSP. - Inject the repo
[project]version into the frontend build/test via__APP_VERSION__, and use it as the masthead’s truthful fallback (instead ofv1.0/app/package.json). - Add Vitest coverage to ensure the fallback never regresses to
v1.0and that the injected version matchespyproject.toml.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| CHANGELOG.md | Documents the masthead/CSP root cause and the new truthful fallback behavior. |
| app/security-headers.conf | Adds https://api.github.com to connect-src and documents why it’s needed. |
| app/vite.config.ts | Injects __APP_VERSION__ from pyproject.toml at build time. |
| app/vitest.config.ts | Mirrors the same __APP_VERSION__ injection for the test build pipeline. |
| app/project-version.ts | Implements the Node-side version reader from repo-root pyproject.toml. |
| app/tsconfig.node.json | Ensures Node-side TS configs include the new config/helper files. |
| app/src/vite-env.d.ts | Declares the injected __APP_VERSION__ global for TypeScript. |
| app/src/global-config.ts | Switches CONFIG.appVersion to the injected version instead of package.json. |
| app/src/global-config.test.ts | Asserts injected version matches pyproject.toml and looks like semver. |
| app/src/layouts/MastheadRule.tsx | Uses v${CONFIG.appVersion} as the null/unresolved release-tag fallback. |
| app/src/layouts/MastheadRule.test.tsx | Adds tests for the fallback version and unresolved-tag link target. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`yarn type-check` runs a second pass over the tests via tsconfig.test.json, whose `types` is limited to vite/client and vitest/globals. The frontend does not depend on @types/node, so `node:fs`, `node:path` and `process` in global-config.test.ts failed that pass (TS2591) — my local check only ran tsc against tsconfig.json, which excludes tests. Re-reading pyproject.toml in the suite was belt-and-braces anyway: the file is parsed in exactly one place, and readProjectVersion() already throws at build time when the [project] version is missing. The test keeps the part that earns its place — that `__APP_VERSION__` is injected and reaches CONFIG.appVersion as a real semver — with no new dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hw82Vmq7yUMk8YoXTfSzBV
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (3)
app/project-version.ts:23
- The parser does not actually ensure the matched
version = ...line is inside the[project]table:^version\s*=can match any table'sversionkey at column 0. This makes the preceding comment and the thrown error message (“No [project] version found…”) inaccurate, and could silently pick the wrong version if another top-levelversionfield is added later. Consider explicitly scoping the search to the[project]section (or adjust the comment/error to match the actual behavior).
const path = fileURLToPath(new URL('../pyproject.toml', import.meta.url));
// Line-anchored so `python_version = "3.13"` under [tool.mypy] can't match;
// the `[project]` table's own key is the only one at column 0.
const match = /^version\s*=\s*"([^"]+)"/m.exec(readFileSync(path, 'utf8'));
if (!match) throw new Error(`No [project] version found in ${path}`);
CHANGELOG.md:33
- This changelog entry claims that “A test asserts the injected value matches
pyproject.toml, so the two cannot drift”, but the added test only checks thatCONFIG.appVersionlooks like a semver triple. As written, the changelog overstates what is actually enforced by tests.
rate-limits the visitor's IP (60 requests/hour, unauthenticated). It now renders the build-time
project version, injected by Vite from the `[project]` version in `pyproject.toml` — the same
field the release flow bumps — instead of a literal that nobody would think to update. A test
asserts the injected value matches `pyproject.toml`, so the two cannot drift (#10485).
app/src/global-config.test.ts:21
- This test only asserts that
CONFIG.appVersionstarts withX.Y.Z, which does not actually guarantee it was injected frompyproject.toml(e.g., a future switch back topackage.jsonwould still satisfy this). Either tighten the assertion to validate provenance more directly, or adjust surrounding docs/changelog text to avoid claiming drift-proofing via this test.
it('is injected from pyproject.toml as a semver triple', () => {
expect(CONFIG.appVersion).toMatch(/^\d+\.\d+\.\d+/);
});
## Summary - **#10485 broke the Cloud Build frontend deploy.** It read the version from the repo-root `pyproject.toml` at build time, but the frontend image is built with `docker build -f app/Dockerfile app` — the build context is `app/` alone, so nothing above it exists inside the builder. `vite build` died with `ENOENT .../pyproject.toml`, Cloud Build failed, and Cloud Run kept serving the previous revision. Every PR check on #10485 was green while the CSP fix never reached anyplot.ai: the exact deploy blind spot `CLAUDE.md` calls out. My mistake — the four CI commands I validated against all run inside a full checkout, so none of them exercises the Docker build context. - **The version now comes from `app/package.json`**, which is always inside the build context. - **The drift that caused the original bug is closed structurally, not by discipline.** Using `package.json` reintroduces a second version field — the same one that sat at `2.0.0` through the whole 3.x line. `tests/unit/test_version_sync.py` asserts it equals the `pyproject.toml` `[project]` version and runs on every PR that touches `pyproject.toml`, which is every release PR. Step 4 of `agentic/commands/release.md` now names the second bump, with the test as the backstop if someone forgets. - **Bonus fix while in the file:** `global-config.ts` imported `package.json` as a default import, which inlined the entire manifest — every dependency name and version range — into a shipped chunk. It now imports only the `version` named export. The user-facing fix from #10485 is unchanged: `api.github.com` stays allowed in `connect-src`, and the masthead still falls back to the real project version instead of a hardcoded `'v1.0'`. ## Test plan - [x] **Reproduced the deploy failure**, since no CI job covers it: hid `pyproject.toml` and ran `yarn build` — fails with `ENOENT ... /pyproject.toml` on the merged code, exits 0 on this branch. This is the Cloud Build condition. - [x] Verified the emitted bundle: `appVersion` is `3.1.0`, the `'v1.0'` literal is absent, and the dependency list no longer appears in any chunk (checked for `react-force-graph-2d` across `dist/assets/*.js`). - [x] **Verified the drift guard actually fires** — set `app/package.json` back to `2.0.0` and confirmed `test_version_sync.py` fails with a diff naming both versions, then restored it. - [x] The repo's four frontend CI commands, green: `yarn lint`, `yarn fm:check`, `yarn type-check`, `yarn test --coverage` (69 files / 617 tests). - [x] `uv run pytest tests/unit/test_version_sync.py` — 2 passed. - [ ] Still not verifiable pre-merge: after this merges, the Cloud Build deploy has to actually succeed. Confirm with `curl -sSI https://anyplot.ai/ | grep -i content-security-policy` listing `https://api.github.com`, then a cold load (cleared `localStorage`) showing `v3.1.0`. ## Checklist - [x] `CHANGELOG.md` updated under `[Unreleased]` — two `### Fixed` bullets with PR refs. - [x] Related documentation updated — `agentic/commands/release.md` step 4 gained the `app/package.json` bump; the reasoning for reading `package.json` rather than `pyproject.toml` is documented at the point of use in `global-config.ts` and in the test's module docstring. --- _Generated by [Claude Code](https://claude.ai/code/session_01Hw82Vmq7yUMk8YoXTfSzBV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
~/anyplot.aifrom the GitHub releases API (useLatestRelease.ts). TheContent-Security-Policyshipped in the 2026-07-16 security-headers audit never listedapi.github.comunderconnect-src, so every browser blocked the request, the hook resolved tonull, and the masthead rendered its hardcoded'v1.0'fallback. Verified against the live header onanyplot.ai—connect-srcwas'self' api.anyplot.ai storage.googleapis.com plausible.io. v3.0.0 and v3.1.0 both shipped to a site advertising v1.0; returning visitors kept seeing a plausible number from theirlocalStoragecache, which is why it survived two releases.[project]version frompyproject.toml(the fieldagentic/commands/release.mdbumps) as__APP_VERSION__, and the masthead falls back to that.CONFIG.appVersionnow tracks a field someone actually bumps — it readapp/package.json, private npm metadata that sat at2.0.0through the whole 3.x line. It uses the injected value instead;pyproject.tomlstays the single source of truth, so no new sync duty is introduced.The GitHub release for v3.1.0 itself is fine — published, not a draft, not a prerelease. Nothing on the release side needed changing.
Test plan
yarn lint,yarn fm:check,yarn type-check,yarn test --coverage.MastheadRulecases — the fallback renders the build-time project version and neverv1.0, and the release slot links to the releases index while the tag is unresolved — plusglobal-config.test.tsasserting__APP_VERSION__reachesCONFIG.appVersionas a real semver.yarn build— production build succeeds; the emitted chunk containsappVersion:"3.1.0"and the'v1.0'literal is gone fromdist/.nginx.conf—security-headers.confis included at server level and re-included in everyadd_headerlocation, so the single edit covers all of them.curl -sSI https://anyplot.ai/ | grep -i content-security-policylistshttps://api.github.com, and that a cold load with clearedlocalStorageshowsv3.1.0.Note on the first CI failure
The initial commit failed
Run Frontend Tests. That was a real defect in this PR, not a flake:yarn type-checkruns a second pass over the tests viatsconfig.test.json, whosetypesis limited tovite/clientandvitest/globals, and my version test readpyproject.tomlthroughnode:fs— the frontend has no@types/nodedependency (TS2591). My local check had only runtsc -p tsconfig.json, which excludes tests. Fixed in b3ea310 by dropping the file read:pyproject.tomlis parsed in exactly one place andreadProjectVersion()already throws at build time when the[project]version is missing, so the suite keeps only the assertion that earns its place — no new dependency added.Checklist
CHANGELOG.mdupdated under[Unreleased]— two### Fixedbullets with PR refs.app/security-headers.confdocuments whyapi.github.comis allowed and what silently breaks without it. Nodocs/page covers the masthead version or the CSP allowlist.