Skip to content

fix(app): the masthead has been claiming v1.0 since the CSP rollout - #10485

Merged
MarkusNeusinger merged 3 commits into
mainfrom
claude/website-version-display-bug-7oxnh6
Aug 19, 2026
Merged

fix(app): the masthead has been claiming v1.0 since the CSP rollout#10485
MarkusNeusinger merged 3 commits into
mainfrom
claude/website-version-display-bug-7oxnh6

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Root cause: the CSP blocked the version lookup. The masthead reads the version beside ~/anyplot.ai from the GitHub releases API (useLatestRelease.ts). The Content-Security-Policy shipped in the 2026-07-16 security-headers audit never listed api.github.com under connect-src, so every browser blocked the request, the hook resolved to null, and the masthead rendered its hardcoded 'v1.0' fallback. Verified against the live header on anyplot.aiconnect-src was '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 their localStorage cache, which is why it survived two releases.
  • The fallback no longer invents a number. It is on screen during every cold load while the fetch is in flight, and stays there whenever GitHub is unreachable or rate-limits the visitor's IP (60 req/h unauthenticated) — so a CSP-only fix would leave a literal that lies again on the next rate limit. Vite now injects the [project] version from pyproject.toml (the field agentic/commands/release.md bumps) as __APP_VERSION__, and the masthead falls back to that.
  • CONFIG.appVersion now tracks a field someone actually bumps — it read app/package.json, private npm metadata that sat at 2.0.0 through the whole 3.x line. It uses the injected value instead; pyproject.toml stays 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

  • The repo's four frontend CI commands, run locally and green: yarn lint, yarn fm:check, yarn type-check, yarn test --coverage.
  • Full suite passing (69 files / 617 tests), including two new MastheadRule cases — the fallback renders the build-time project version and never v1.0, and the release slot links to the releases index while the tag is unresolved — plus global-config.test.ts asserting __APP_VERSION__ reaches CONFIG.appVersion as a real semver.
  • yarn build — production build succeeds; the emitted chunk contains appVersion:"3.1.0" and the 'v1.0' literal is gone from dist/.
  • CSP change reviewed against nginx.confsecurity-headers.conf is included at server level and re-included in every add_header location, so the single edit covers all of them.
  • Not verifiable pre-merge: the header change only takes effect once the frontend image is rebuilt and deployed. After the Cloud Build deploy, confirm curl -sSI https://anyplot.ai/ | grep -i content-security-policy lists https://api.github.com, and that a cold load with cleared localStorage shows v3.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-check runs a second pass over the tests via tsconfig.test.json, whose types is limited to vite/client and vitest/globals, and my version test read pyproject.toml through node:fs — the frontend has no @types/node dependency (TS2591). My local check had only run tsc -p tsconfig.json, which excludes tests. Fixed in b3ea310 by dropping the file read: pyproject.toml is parsed in exactly one place and readProjectVersion() 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.md updated under [Unreleased] — two ### Fixed bullets with PR refs.
  • Related documentation updated — the CSP notes block in app/security-headers.conf documents why api.github.com is allowed and what silently breaks without it. No docs/ page covers the masthead version or the CSP allowlist.

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
Copilot AI lite review requested due to automatic review settings August 19, 2026 06:09
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hw82Vmq7yUMk8YoXTfSzBV

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 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.com in connect-src so useLatestRelease can 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 of v1.0 / app/package.json).
  • Add Vitest coverage to ensure the fallback never regresses to v1.0 and that the injected version matches pyproject.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
Copilot AI review requested due to automatic review settings August 19, 2026 06:14
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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 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's version key 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-level version field 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 that CONFIG.appVersion looks 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.appVersion starts with X.Y.Z, which does not actually guarantee it was injected from pyproject.toml (e.g., a future switch back to package.json would 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+/);
  });

@MarkusNeusinger
MarkusNeusinger merged commit 3e4d460 into main Aug 19, 2026
10 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the claude/website-version-display-bug-7oxnh6 branch August 19, 2026 06:43
MarkusNeusinger added a commit that referenced this pull request Aug 19, 2026
## 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>
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.

3 participants