Skip to content

fix(deploy): CORP, a narrow CSP, immutable asset caching and a deploy check - #1859

Merged
magic-peach merged 3 commits into
mainfrom
feat/deploy-headers
Nov 5, 2026
Merged

magic-peach merged 3 commits into
mainfrom
feat/deploy-headers

Conversation

@magic-peach

Copy link
Copy Markdown
Collaborator

Phase 6: the deploy headers and caching work from the old feat/deploy-hardening / fix/release-hardening branches, ported as a fresh PR on main. Commit-stamped builds were already on main (NEXT_PUBLIC_RELEASE); this adds the rest.

Headers

All three hosts get the same set: netlify.toml (canonical), public/_headers and vercel.json. deployConfig.test.ts keeps them identical.

  • Cross-Origin-Resource-Policy: same-origin: other sites can't embed Reframe's files.
  • Content-Security-Policy: base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'
    • There are no fetch directives (script-src, connect-src …), so the policy can't block the FFmpeg cores or Datadog RUM.
    • It stops framing, plugins, <base> hijacking and off-site form posts. The footer's email form doesn't submit anywhere, so form-action 'self' is safe.
    • The existing test ("no CSP that could block the CDN or RUM") now asserts exactly these four directives instead of no CSP at all.
  • Asset caching: Cache-Control: public, max-age=31536000, immutable on /_next/static/*, whose names change with their content. Before, this depended on the Netlify Next.js plugin being enabled in the UI; a deploy without the plugin served them with max-age=0.
  • COOP/COEP: unchanged.

Deployment check

  • scripts/deploy/check-deploy.mjs <url> [--expect-commit <sha>] [--json], with pure rules in scripts/deploy/lib/deployCheck.mjs.
    • What it requests: the page, a hashed script, the preloaded font, the FFmpeg worker chunk (found through the webpack runtime), the engine WASM on jsDelivr (it checks CORS, which COEP needs), the /privacy/ → /privacy redirect and a 404.
    • What it checks: headers, MIME types and caching on each response.
    • Commit check: with --expect-commit, it compares the build stamp in <meta name="reframe-build"> (added to the layout from NEXT_PUBLIC_RELEASE). A stale deploy, an unstamped one or a dev one fails.
    • Exit codes: 0 pass, 1 fail, 2 usage error, 3 the deployment needs a login, 4 unreachable.
  • .github/workflows/deploy-smoke.yml, read-only with no secrets:
    • On every PR and on main, it builds, serves out/ locally with the declared headers and runs the check with --expect-commit $GITHUB_SHA.
    • On Netlify status events it checks the deploy preview. GitHub only runs status-triggered workflows from the default branch, so that part starts working once this is on main.
  • scripts/serve-isolated.mjs (Playwright and the smoke test) now sends the declared headers from vercel.json instead of only COOP/COEP. It also sets the hosts' caching and the trailing-slash redirect. ISOLATED=0 still drops COOP/COEP for the single-threaded project.

Tests

  • src/lib/tests/deployCheck.test.ts: the rules, plus the CLI run against a real local server (good, missing header, stale commit, needs login, unreachable, bad usage).
  • deployConfig.test.ts: CORP, the CSP's directives, and immutable caching in both Netlify files.
  • vitest.setup.ts skips the window polyfill in node-environment tests.
  • All 567 unit tests pass.
  • A local commit-stamped build passes every check. The full Playwright suite passes under the new headers (46 passed, 8 optional large-file runs skipped).

Docs

docs/DEPLOYMENT.md: the deploy check, the smoke workflow, and what each header is for.

After this is merged I'll run the check against the live site.

… check

Ported from the old deploy-hardening branch onto main:
- every host sends Cross-Origin-Resource-Policy: same-origin and a CSP
  with no fetch directives (base-uri, object-src, frame-ancestors,
  form-action), so it can't block the FFmpeg cores or analytics;
- /_next/static/* is cached for a year as immutable, declared rather than
  left to the Netlify Next.js plugin;
- scripts/deploy/check-deploy.mjs checks a deployed or local site: headers,
  MIME types and caching on the page, a script, the font, the FFmpeg
  worker chunk, the engine WASM, the trailing-slash redirect and a 404,
  and with --expect-commit that the build is the right one (stamped in
  <meta name="reframe-build">);
- deploy-smoke.yml runs it on every PR against a local build and against
  Netlify deploy previews;
- the local test server sends the declared headers, caching and redirect.
@netlify

netlify Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for reframe-os ready!

Name Link
🔨 Latest commit 68a358e
🔍 Latest deploy log https://app.netlify.com/projects/reframe-os/deploys/6aecdb9b5ab3e45747c8f75b
😎 Deploy Preview https://deploy-preview-1859--reframe-os.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@magic-peach
magic-peach merged commit f8a2c93 into main Nov 5, 2026
18 checks passed
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.

1 participant