From 0666c74f69e0d35e21c8cba047a4bdc02048734f Mon Sep 17 00:00:00 2001 From: MarketDataApp Date: Wed, 25 Feb 2026 14:05:40 -0300 Subject: [PATCH 1/4] refactor: migrate staging from /docs-staging/ path to www-staging subdomain Staging now lives at www-staging.marketdata.app/docs/ instead of www.marketdata.app/docs-staging/, letting both environments use identical /docs/ paths. Worker uses hostname-based routing instead of path-prefix matching, eliminating matchesRoute() and the Algolia search path rewrite. --- .github/workflows/deploy-docs.yml | 3 +- CLAUDE.md | 6 +- README.md | 4 +- docusaurus.config.js | 26 ++---- e2e/context7-widget.spec.js | 2 +- worker/handler.integration.test.js | 35 +++---- worker/handler.js | 133 +++++++++++---------------- worker/handler.test.js | 105 ++++++++++----------- worker/redirects.integration.test.js | 2 +- worker/wrangler.toml | 4 +- 10 files changed, 136 insertions(+), 184 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index a376ebac..aee59b38 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -27,14 +27,13 @@ jobs: env: GIT_REF: ${{ github.ref }} run: | + echo "base_url=docs" >> "$GITHUB_OUTPUT" if [ "$GIT_REF" = "refs/heads/main" ]; then echo "prod=true" >> "$GITHUB_OUTPUT" - echo "base_url=docs" >> "$GITHUB_OUTPUT" echo "project=marketdata-docs" >> "$GITHUB_OUTPUT" echo "environment=production" >> "$GITHUB_OUTPUT" else echo "prod=" >> "$GITHUB_OUTPUT" - echo "base_url=docs-staging" >> "$GITHUB_OUTPUT" echo "project=marketdata-docs-staging" >> "$GITHUB_OUTPUT" echo "environment=staging" >> "$GITHUB_OUTPUT" fi diff --git a/CLAUDE.md b/CLAUDE.md index 0632786a..0ef4de6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,14 +4,14 @@ - Docs site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse proxy - Worker (`worker/`) proxies `www.marketdata.app/docs/*` → `marketdata-docs.pages.dev` -- Staging: `www.marketdata.app/docs-staging/*` → `marketdata-docs-staging.pages.dev` +- Staging: `www-staging.marketdata.app/docs/*` → `marketdata-docs-staging.pages.dev` - CI/CD: GitHub Actions (`.github/workflows/deploy-docs.yml`) builds Docusaurus, restructures output to match URL paths, and deploys to Cloudflare Pages + Worker via Wrangler -- Build output is restructured in CI to nest under `docs/` or `docs-staging/` so static files serve from correct paths without rewrite rules +- Build output is restructured in CI to nest under `docs/` so static files serve from correct paths without rewrite rules - Edge caching enabled on Worker subrequests; `_headers` file generated in CI for asset cache control ## Workflow -- Work on the **staging** branch, verify changes at `www.marketdata.app/docs-staging/` +- Work on the **staging** branch, verify changes at `www-staging.marketdata.app/docs/` - Once verified, open a PR from `staging` → `main` and merge to deploy to production ## Package Manager diff --git a/README.md b/README.md index ea589bc3..446b8ad7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ The official documentation for [Market Data](https://www.marketdata.app/) — covering the REST API, SDKs, and Google Sheets Add-On. Built with [Docusaurus 3](https://docusaurus.io/). **Production:** [www.marketdata.app/docs/](https://www.marketdata.app/docs/) -**Staging:** [www.marketdata.app/docs-staging/](https://www.marketdata.app/docs-staging/) +**Staging:** [www-staging.marketdata.app/docs/](https://www-staging.marketdata.app/docs/) ## Documentation Sections @@ -30,7 +30,7 @@ The site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse 2. Verify changes at the staging URL 3. Open a PR from `staging` → `main` and merge — deploys to production -The CI pipeline (`.github/workflows/deploy-docs.yml`) builds the Docusaurus site, restructures the output to match the `/docs/` and `/docs-staging/` URL paths, generates cache headers, and deploys via Wrangler. +The CI pipeline (`.github/workflows/deploy-docs.yml`) builds the Docusaurus site, restructures the output to nest under `/docs/`, generates cache headers, and deploys via Wrangler. ## Project Structure diff --git a/docusaurus.config.js b/docusaurus.config.js index 5ef2ebaa..266c20e3 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -15,9 +15,9 @@ const config = { url: process.env.PROD == "true" ? "https://www.marketdata.app/" - : "https://www.marketdata.app/", + : "https://www-staging.marketdata.app/", - baseUrl: process.env.PROD == "true" ? "/docs/" : "/docs-staging/", + baseUrl: "/docs/", noIndex: process.env.PROD !== "true", onBrokenLinks: "ignore", onBrokenMarkdownLinks: "warn", @@ -121,8 +121,8 @@ const config = { sidebarPath: require.resolve("./sidebars.js"), editUrl: ({ docPath }) => { - const base = process.env.PROD == "true" ? "/docs" : "/docs-staging"; - return `https://www.marketdata.app${base}/api/${docPath.replace(/\.mdx?$/, '.md')}`; + const host = process.env.PROD == "true" ? "www.marketdata.app" : "www-staging.marketdata.app"; + return `https://${host}/docs/api/${docPath.replace(/\.mdx?$/, '.md')}`; }, }, ], @@ -134,8 +134,8 @@ const config = { path: "sdk", routeBasePath: "sdk", editUrl: ({ docPath }) => { - const base = process.env.PROD == "true" ? "/docs" : "/docs-staging"; - return `https://www.marketdata.app${base}/sdk/${docPath.replace(/\.mdx?$/, '.md')}`; + const host = process.env.PROD == "true" ? "www.marketdata.app" : "www-staging.marketdata.app"; + return `https://${host}/docs/sdk/${docPath.replace(/\.mdx?$/, '.md')}`; }, sidebarPath: require.resolve("./sidebars.js"), }, @@ -148,8 +148,8 @@ const config = { path: "sheets", routeBasePath: "sheets", editUrl: ({ docPath }) => { - const base = process.env.PROD == "true" ? "/docs" : "/docs-staging"; - return `https://www.marketdata.app${base}/sheets/${docPath.replace(/\.mdx?$/, '.md')}`; + const host = process.env.PROD == "true" ? "www.marketdata.app" : "www-staging.marketdata.app"; + return `https://${host}/docs/sheets/${docPath.replace(/\.mdx?$/, '.md')}`; }, sidebarPath: require.resolve("./sidebars.js"), }, @@ -162,8 +162,8 @@ const config = { path: "account", routeBasePath: "account", editUrl: ({ docPath }) => { - const base = process.env.PROD == "true" ? "/docs" : "/docs-staging"; - return `https://www.marketdata.app${base}/account/${docPath.replace(/\.mdx?$/, '.md')}`; + const host = process.env.PROD == "true" ? "www.marketdata.app" : "www-staging.marketdata.app"; + return `https://${host}/docs/account/${docPath.replace(/\.mdx?$/, '.md')}`; }, sidebarPath: require.resolve("./sidebars.js"), }, @@ -177,12 +177,6 @@ const config = { appId: "IUHZFO750H", apiKey: "c29b76b827a4fa1a0ac3abe15f69ec5c", indexName: "Market Data Documentation", - ...(process.env.PROD !== "true" && { - replaceSearchResultPathname: { - from: "/docs/", - to: "/docs-staging/", - }, - }), }, navbar: { diff --git a/e2e/context7-widget.spec.js b/e2e/context7-widget.spec.js index c854de75..ae1f60b9 100644 --- a/e2e/context7-widget.spec.js +++ b/e2e/context7-widget.spec.js @@ -10,7 +10,7 @@ import { test, expect } from '@playwright/test'; const BASE_URL = process.env.TEST_ENV === 'staging' - ? 'https://www.marketdata.app/docs-staging' + ? 'https://www-staging.marketdata.app/docs' : 'https://www.marketdata.app/docs'; // One representative page per widget configuration. diff --git a/worker/handler.integration.test.js b/worker/handler.integration.test.js index 873f83b9..4d69d8f9 100644 --- a/worker/handler.integration.test.js +++ b/worker/handler.integration.test.js @@ -7,21 +7,22 @@ * * Note: The production sitemap is always used as the URL source because * staging has noIndex enabled which suppresses sitemap generation. URLs - * are rewritten to the target environment's base path. + * are rewritten to the target environment's host. * * Run with: yarn test:integration - * Requires network access to www.marketdata.app + * Requires network access to www.marketdata.app and www-staging.marketdata.app */ import { describe, it, expect } from 'vitest'; const PROD_SITEMAP_URL = 'https://www.marketdata.app/docs/sitemap.xml'; -const PROD_BASE = '/docs'; const ENVIRONMENTS = { - staging: { basePath: '/docs-staging' }, - production: { basePath: '/docs' }, + staging: { host: 'https://www-staging.marketdata.app' }, + production: { host: 'https://www.marketdata.app' }, }; +const BASE_PATH = '/docs'; + const TEST_ENV = process.env.TEST_ENV; const envs = TEST_ENV ? { [TEST_ENV]: ENVIRONMENTS[TEST_ENV] } : ENVIRONMENTS; @@ -44,9 +45,9 @@ const SKIP_SUFFIX_EXACT = [ '/sdk/options', ]; -function shouldSkip(path, basePath) { - if (SKIP_SUFFIX_EXACT.some((suffix) => path === `${basePath}${suffix}`)) return true; - if (SKIP_SUFFIX_PATTERNS.some((suffix) => path.startsWith(`${basePath}${suffix}`))) return true; +function shouldSkip(path) { + if (SKIP_SUFFIX_EXACT.some((suffix) => path === `${BASE_PATH}${suffix}`)) return true; + if (SKIP_SUFFIX_PATTERNS.some((suffix) => path.startsWith(`${BASE_PATH}${suffix}`))) return true; return SKIP_CONTAINS.some((pattern) => path.includes(pattern)); } @@ -60,25 +61,17 @@ async function fetchSitemapUrls() { return urls; } -/** Rewrite a production path to the target environment's base path. */ -function rebasePath(prodPath, targetBasePath) { - return prodPath.replace(PROD_BASE, targetBasePath); -} - -for (const [env, { basePath }] of Object.entries(envs)) { +for (const [env, { host }] of Object.entries(envs)) { describe(`markdown serving — ${env} (live sitemap)`, async () => { - const prodPaths = await fetchSitemapUrls(); - const targetPaths = prodPaths - .map((p) => rebasePath(p, basePath)) - .filter((p) => !shouldSkip(p, basePath)); + const paths = (await fetchSitemapUrls()).filter((p) => !shouldSkip(p)); it(`sitemap has URLs to test`, () => { - expect(targetPaths.length).toBeGreaterThan(50); + expect(paths.length).toBeGreaterThan(50); }); - for (const path of targetPaths) { + for (const path of paths) { it(`${path} returns markdown`, async () => { - const url = `https://www.marketdata.app${path}`; + const url = `${host}${path}`; const res = await fetch(url, { headers: { Accept: 'text/markdown' }, redirect: 'follow', diff --git a/worker/handler.js b/worker/handler.js index 23e4d9cb..775b8872 100644 --- a/worker/handler.js +++ b/worker/handler.js @@ -1,14 +1,12 @@ /** - * Cloudflare Worker to proxy /docs/ and /docs-staging/ paths - * from www.marketdata.app to Cloudflare Pages deployments. - * - * Last deployed: 2026-02-24 + * Cloudflare Worker to proxy /docs/ paths from www.marketdata.app + * and www-staging.marketdata.app to Cloudflare Pages deployments. * * Routing: * - www.marketdata.app/docs/* → marketdata-docs.pages.dev/docs/* - * - www.marketdata.app/docs-staging/* → marketdata-docs-staging.pages.dev/docs-staging/* + * - www-staging.marketdata.app/docs/* → marketdata-docs-staging.pages.dev/docs/* * - * The path is preserved as-is (including the /docs/ or /docs-staging/ prefix). + * The path is preserved as-is (including the /docs/ prefix). * Cloudflare Pages serves files from a matching directory structure in the * build output, so no path rewriting is needed. * @@ -16,35 +14,13 @@ * are passed through unchanged. */ -const ORIGINAL_HOSTNAME = 'www.marketdata.app'; - /** - * Route definitions mapping URL path prefixes to Cloudflare Pages targets. - * Order matters: /docs-staging/ must come before /docs/ to avoid - * /docs-staging/* being matched by the /docs/ prefix. + * Hostname-based routing: maps each hostname to its Cloudflare Pages target. */ -const ROUTES = [ - { - prefix: '/docs-staging/', - target: 'marketdata-docs-staging.pages.dev', - }, - { - prefix: '/docs/', - target: 'marketdata-docs.pages.dev', - }, -]; - -/** - * Checks whether a URL path matches a route, handling both - * trailing-slash (/docs-staging/) and no-trailing-slash (/docs-staging) forms. - * - * @param {string} pathname - The URL path to check (e.g. "/docs-staging/api") - * @param {string} prefix - The route prefix to match (e.g. "/docs-staging/") - * @returns {boolean} - */ -function matchesRoute(pathname, prefix) { - return pathname.startsWith(prefix) || pathname === prefix.slice(0, -1); -} +const TARGETS = { + 'www.marketdata.app': 'marketdata-docs.pages.dev', + 'www-staging.marketdata.app': 'marketdata-docs-staging.pages.dev', +}; /** * Converts raw MDX/markdown source into clean markdown by stripping @@ -78,7 +54,7 @@ function cleanMarkdown(text) { /** * Routes incoming requests to the appropriate Cloudflare Pages deployment - * based on the URL path. Non-matching requests pass through to the + * based on the hostname. Non-matching requests pass through to the * default origin (WordPress on cPanel). * * @param {Request} request - The incoming request @@ -86,8 +62,15 @@ function cleanMarkdown(text) { */ async function handleRequest(request) { const url = new URL(request.url); + const target = TARGETS[url.hostname]; + + if (!target) { + return fetch(request); + } + + const isDocsPath = url.pathname.startsWith('/docs/') || url.pathname === '/docs'; - if (url.hostname !== ORIGINAL_HOSTNAME) { + if (!isDocsPath) { return fetch(request); } @@ -101,58 +84,48 @@ async function handleRequest(request) { } // Serve raw markdown for .md URLs or Accept: text/markdown header - const docsPrefix = url.pathname.startsWith('/docs-staging/') ? '/docs-staging/' - : url.pathname.startsWith('/docs/') ? '/docs/' : null; - - if (docsPrefix) { - const wantsMd = url.pathname.endsWith('.md'); - const acceptsMd = (request.headers.get('accept') || '').includes('text/markdown'); - - if (wantsMd || acceptsMd) { - const stem = wantsMd - ? url.pathname.slice(docsPrefix.length, -3) - : url.pathname.replace(/\/$/, '').slice(docsPrefix.length); - const branch = docsPrefix === '/docs-staging/' ? 'staging' : 'main'; - const base = `https://raw.githubusercontent.com/MarketDataApp/documentation/${branch}`; - const candidates = [ - `${base}/${stem}.md`, - `${base}/${stem}.mdx`, - `${base}/${stem}/index.md`, - `${base}/${stem}/index.mdx`, - ]; - for (const candidate of candidates) { - const res = await fetch(candidate); - if (res.ok) { - const text = cleanMarkdown(await res.text()); - return new Response(text, { - headers: { 'content-type': 'text/markdown; charset=utf-8' }, - }); - } + const wantsMd = url.pathname.endsWith('.md'); + const acceptsMd = (request.headers.get('accept') || '').includes('text/markdown'); + + if (wantsMd || acceptsMd) { + const docsPrefix = '/docs/'; + const stem = wantsMd + ? url.pathname.slice(docsPrefix.length, -3) + : url.pathname.replace(/\/$/, '').slice(docsPrefix.length); + const branch = url.hostname === 'www-staging.marketdata.app' ? 'staging' : 'main'; + const base = `https://raw.githubusercontent.com/MarketDataApp/documentation/${branch}`; + const candidates = [ + `${base}/${stem}.md`, + `${base}/${stem}.mdx`, + `${base}/${stem}/index.md`, + `${base}/${stem}/index.mdx`, + ]; + for (const candidate of candidates) { + const res = await fetch(candidate); + if (res.ok) { + const text = cleanMarkdown(await res.text()); + return new Response(text, { + headers: { 'content-type': 'text/markdown; charset=utf-8' }, + }); } } } - for (const route of ROUTES) { - if (matchesRoute(url.pathname, route.prefix)) { - // Docs sites don't serve robots.txt; block stale cached copies - if (url.pathname.endsWith('/robots.txt')) { - return new Response('', { status: 404 }); - } - - url.hostname = route.target; - const response = await fetch(new Request(url, request), { cf: { cacheEverything: true } }); + // Docs sites don't serve robots.txt; block stale cached copies + if (url.pathname.endsWith('/robots.txt')) { + return new Response('', { status: 404 }); + } - if (response.status === 404) { - const pathname = new URL(request.url).pathname; - const referer = request.headers.get('referer'); - console.log({ level: '404', message: pathname, referer: referer || '' }); - } + url.hostname = target; + const response = await fetch(new Request(url, request), { cf: { cacheEverything: true } }); - return response; - } + if (response.status === 404) { + const pathname = new URL(request.url).pathname; + const referer = request.headers.get('referer'); + console.log({ level: '404', message: pathname, referer: referer || '' }); } - return fetch(request); + return response; } -module.exports = { handleRequest, matchesRoute, cleanMarkdown, ROUTES, ORIGINAL_HOSTNAME }; +module.exports = { handleRequest, cleanMarkdown, TARGETS }; diff --git a/worker/handler.test.js b/worker/handler.test.js index 7a69bb38..1880d116 100644 --- a/worker/handler.test.js +++ b/worker/handler.test.js @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); -const { handleRequest, matchesRoute, cleanMarkdown } = require('./handler'); +const { handleRequest, cleanMarkdown } = require('./handler'); function makeRequest(url, options = {}) { return new Request(url, options); @@ -14,30 +14,6 @@ beforeEach(() => { vi.clearAllMocks(); }); -// --- matchesRoute --- - -describe('matchesRoute', () => { - it('matches path starting with prefix', () => { - expect(matchesRoute('/docs/api', '/docs/')).toBe(true); - }); - - it('matches exact path without trailing slash', () => { - expect(matchesRoute('/docs', '/docs/')).toBe(true); - }); - - it('does not match unrelated path', () => { - expect(matchesRoute('/other', '/docs/')).toBe(false); - }); - - it('does not match partial prefix', () => { - expect(matchesRoute('/documentation', '/docs/')).toBe(false); - }); - - it('/docs-staging/ does not match /docs/ prefix', () => { - expect(matchesRoute('/docs-staging/', '/docs/')).toBe(false); - }); -}); - // --- cleanMarkdown --- describe('cleanMarkdown', () => { @@ -88,6 +64,37 @@ describe('handleRequest', () => { }); }); + // --- Non-docs paths on known hostnames --- + + describe('non-docs paths on known hostnames', () => { + it('passes through non-docs paths on www.marketdata.app', async () => { + const passthroughResponse = new Response('wordpress'); + mockFetch.mockResolvedValueOnce(passthroughResponse); + const req = makeRequest('https://www.marketdata.app/blog/some-post'); + const res = await handleRequest(req); + expect(mockFetch).toHaveBeenCalledWith(req); + expect(res).toBe(passthroughResponse); + }); + + it('passes through non-docs paths on www-staging.marketdata.app', async () => { + const passthroughResponse = new Response('origin'); + mockFetch.mockResolvedValueOnce(passthroughResponse); + const req = makeRequest('https://www-staging.marketdata.app/other/page'); + const res = await handleRequest(req); + expect(mockFetch).toHaveBeenCalledWith(req); + expect(res).toBe(passthroughResponse); + }); + + it('passes through root path', async () => { + const passthroughResponse = new Response('homepage'); + mockFetch.mockResolvedValueOnce(passthroughResponse); + const req = makeRequest('https://www.marketdata.app/'); + const res = await handleRequest(req); + expect(mockFetch).toHaveBeenCalledWith(req); + expect(res).toBe(passthroughResponse); + }); + }); + // --- SDK PHP redirects --- describe('SDK PHP redirect', () => { @@ -186,9 +193,9 @@ describe('handleRequest', () => { expect(text).toContain('# Found'); }); - it('serves markdown for docs-staging URLs', async () => { + it('serves markdown for staging URLs (www-staging hostname)', async () => { mockFetch.mockResolvedValueOnce(new Response('# Staging\n', { status: 200 })); - const req = makeRequest('https://www.marketdata.app/docs-staging/api/stocks.md'); + const req = makeRequest('https://www-staging.marketdata.app/docs/api/stocks.md'); const res = await handleRequest(req); expect(res.headers.get('content-type')).toBe('text/markdown; charset=utf-8'); // Should fetch from staging branch @@ -196,7 +203,7 @@ describe('handleRequest', () => { expect(fetchedUrl).toContain('/staging/'); }); - it('fetches from main branch for /docs/ URLs', async () => { + it('fetches from main branch for www.marketdata.app URLs', async () => { mockFetch.mockResolvedValueOnce(new Response('# Prod\n', { status: 200 })); const req = makeRequest('https://www.marketdata.app/docs/api/stocks.md'); await handleRequest(req); @@ -221,15 +228,15 @@ describe('handleRequest', () => { // --- robots.txt --- describe('robots.txt blocking', () => { - it('returns 404 for /docs/robots.txt', async () => { + it('returns 404 for /docs/robots.txt on production', async () => { const req = makeRequest('https://www.marketdata.app/docs/robots.txt'); const res = await handleRequest(req); expect(res.status).toBe(404); expect(mockFetch).not.toHaveBeenCalled(); }); - it('returns 404 for /docs-staging/robots.txt', async () => { - const req = makeRequest('https://www.marketdata.app/docs-staging/robots.txt'); + it('returns 404 for /docs/robots.txt on staging', async () => { + const req = makeRequest('https://www-staging.marketdata.app/docs/robots.txt'); const res = await handleRequest(req); expect(res.status).toBe(404); expect(mockFetch).not.toHaveBeenCalled(); @@ -239,7 +246,7 @@ describe('handleRequest', () => { // --- Route proxying --- describe('route proxying', () => { - it('proxies /docs/* to marketdata-docs.pages.dev', async () => { + it('proxies /docs/* on www.marketdata.app to production Pages', async () => { mockFetch.mockResolvedValueOnce(new Response('page', { status: 200 })); const req = makeRequest('https://www.marketdata.app/docs/api/stocks'); await handleRequest(req); @@ -247,9 +254,9 @@ describe('handleRequest', () => { expect(new URL(calledWith.url).hostname).toBe('marketdata-docs.pages.dev'); }); - it('proxies /docs-staging/* to staging', async () => { + it('proxies /docs/* on www-staging.marketdata.app to staging Pages', async () => { mockFetch.mockResolvedValueOnce(new Response('page', { status: 200 })); - const req = makeRequest('https://www.marketdata.app/docs-staging/api/stocks'); + const req = makeRequest('https://www-staging.marketdata.app/docs/api/stocks'); await handleRequest(req); const calledWith = mockFetch.mock.calls[0][0]; expect(new URL(calledWith.url).hostname).toBe('marketdata-docs-staging.pages.dev'); @@ -270,6 +277,14 @@ describe('handleRequest', () => { const fetchOptions = mockFetch.mock.calls[0][1]; expect(fetchOptions).toEqual({ cf: { cacheEverything: true } }); }); + + it('handles bare /docs path (no trailing slash)', async () => { + mockFetch.mockResolvedValueOnce(new Response('page', { status: 200 })); + const req = makeRequest('https://www.marketdata.app/docs'); + await handleRequest(req); + const calledWith = mockFetch.mock.calls[0][0]; + expect(new URL(calledWith.url).hostname).toBe('marketdata-docs.pages.dev'); + }); }); // --- 404 logging --- @@ -303,26 +318,4 @@ describe('handleRequest', () => { consoleSpy.mockRestore(); }); }); - - // --- Pass-through --- - - describe('pass-through for non-matching routes', () => { - it('passes through requests to non-docs paths', async () => { - const passthroughResponse = new Response('wordpress'); - mockFetch.mockResolvedValueOnce(passthroughResponse); - const req = makeRequest('https://www.marketdata.app/blog/some-post'); - const res = await handleRequest(req); - expect(mockFetch).toHaveBeenCalledWith(req); - expect(res).toBe(passthroughResponse); - }); - - it('passes through root path', async () => { - const passthroughResponse = new Response('homepage'); - mockFetch.mockResolvedValueOnce(passthroughResponse); - const req = makeRequest('https://www.marketdata.app/'); - const res = await handleRequest(req); - expect(mockFetch).toHaveBeenCalledWith(req); - expect(res).toBe(passthroughResponse); - }); - }); }); diff --git a/worker/redirects.integration.test.js b/worker/redirects.integration.test.js index f7ef1fe7..42b88a4f 100644 --- a/worker/redirects.integration.test.js +++ b/worker/redirects.integration.test.js @@ -16,7 +16,7 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; const ALL_ENVIRONMENTS = { - staging: 'https://www.marketdata.app/docs-staging', + staging: 'https://www-staging.marketdata.app/docs', production: 'https://www.marketdata.app/docs', }; diff --git a/worker/wrangler.toml b/worker/wrangler.toml index 7326f126..46fc8e15 100644 --- a/worker/wrangler.toml +++ b/worker/wrangler.toml @@ -3,8 +3,8 @@ main = "index.js" compatibility_date = "2024-01-01" routes = [ - { pattern = "www.marketdata.app/docs-staging", zone_name = "marketdata.app" }, - { pattern = "www.marketdata.app/docs-staging/*", zone_name = "marketdata.app" }, + { pattern = "www-staging.marketdata.app/docs", zone_name = "marketdata.app" }, + { pattern = "www-staging.marketdata.app/docs/*", zone_name = "marketdata.app" }, { pattern = "www.marketdata.app/docs", zone_name = "marketdata.app" }, { pattern = "www.marketdata.app/docs/*", zone_name = "marketdata.app" }, ] From 786e2974603b1bb1a5b7b07337338fcee75fa256 Mon Sep 17 00:00:00 2001 From: MarketDataApp Date: Wed, 25 Feb 2026 14:22:04 -0300 Subject: [PATCH 2/4] docs: update README and CLAUDE.md with architecture details Add request flow, worker features, environment table, DNS setup, CI pipeline steps, and testing commands. --- CLAUDE.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++----- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ef4de6a..d282a323 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,11 +3,47 @@ ## Hosting & URLs - Docs site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse proxy -- Worker (`worker/`) proxies `www.marketdata.app/docs/*` → `marketdata-docs.pages.dev` -- Staging: `www-staging.marketdata.app/docs/*` → `marketdata-docs-staging.pages.dev` -- CI/CD: GitHub Actions (`.github/workflows/deploy-docs.yml`) builds Docusaurus, restructures output to match URL paths, and deploys to Cloudflare Pages + Worker via Wrangler -- Build output is restructured in CI to nest under `docs/` so static files serve from correct paths without rewrite rules -- Edge caching enabled on Worker subrequests; `_headers` file generated in CI for asset cache control +- Both environments use the same `/docs/` base path — routing is by hostname, not path prefix + +| Environment | URL | Pages Project | Branch | +|-------------|-----|---------------|--------| +| Production | `www.marketdata.app/docs/` | `marketdata-docs` | `main` | +| Staging | `www-staging.marketdata.app/docs/` | `marketdata-docs-staging` | `staging` | + +## Architecture + +### Request flow + +1. DNS resolves the hostname (both are proxied CNAMEs in Cloudflare) +2. Cloudflare routes `/docs` and `/docs/*` to the Worker (via `wrangler.toml` route patterns) +3. Worker (`worker/handler.js`) looks up the hostname in the `TARGETS` map to find the Pages target +4. Worker rewrites the hostname and fetches from the Pages project (e.g. `marketdata-docs.pages.dev/docs/api/stocks`) +5. Pages serves the file from its `docs/` directory (built and nested there by CI) +6. Worker returns the response to the client — path stays the same throughout + +### Worker features (`worker/`) + +- **Hostname-based routing**: `TARGETS` map in `handler.js` maps each hostname to its Cloudflare Pages deployment +- **Markdown serving**: Requests with `.md` extension or `Accept: text/markdown` header fetch raw source from GitHub and return cleaned markdown (frontmatter stripped, JSX components converted) +- **SDK PHP redirect**: `/docs/sdk-php/*` → `marketdataapp.github.io/sdk-php/*` (301) +- **robots.txt blocking**: Returns 404 for `/docs/robots.txt` to prevent stale cached copies +- **Edge caching**: Passes `cf.cacheEverything` on subrequests +- **404 logging**: Logs pathname and referer for 404 responses +- Non-docs paths pass through to the origin (WordPress) + +### CI/CD pipeline (`.github/workflows/deploy-docs.yml`) + +1. Builds Docusaurus (`yarn build`) +2. Restructures build output to nest under `build/docs/` (both environments use same structure) +3. Generates `_headers` file for asset cache control +4. Uploads build to R2 (`www-marketdata-app-builds` bucket) +5. Deploys to Cloudflare Pages via Wrangler +6. If `worker/` files changed: runs worker tests, then deploys the worker + +### DNS + +- `www-staging.marketdata.app` → CNAME to `marketdata-docs-staging.pages.dev` (proxied) +- `www.marketdata.app` → existing DNS (proxied) ## Workflow @@ -29,3 +65,10 @@ - Badges (New, Premium, Beta, High Usage) are configured via `sidebar_custom_props: { badge: n/p/b/h }` in page frontmatter - Rendered by `src/theme/RenderTag.js`, styled in `src/css/custom.css` - Supported in sidebar links, sidebar categories, and page titles via swizzled theme components + +## Testing + +- **Unit tests**: `cd worker && yarn test` — tests worker routing, markdown serving, robots.txt, 404 logging +- **Integration tests**: `cd worker && TEST_ENV=staging yarn test:integration` — fetches live sitemap and verifies markdown serving for every doc URL +- **Redirect tests**: `cd worker && TEST_ENV=staging yarn test:integration` — verifies client-side redirects from `docusaurus.config.js` +- **E2E tests**: `TEST_ENV=staging yarn test:e2e` — Playwright tests for Context7 widget rendering diff --git a/README.md b/README.md index 446b8ad7..f8f428a1 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,62 @@ yarn start # Start dev server at localhost:3000 yarn build # Production build ``` +## Architecture + +The site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse proxy. Both production and staging use the same `/docs/` base path — routing is determined by hostname, not path prefix. + +### Request flow + +``` +Browser → Cloudflare DNS → Worker (hostname lookup) → Cloudflare Pages → Response +``` + +1. DNS resolves the hostname (both are proxied CNAMEs in Cloudflare) +2. Cloudflare routes `/docs` and `/docs/*` to the Worker (via `wrangler.toml` route patterns) +3. Worker looks up the hostname in a `TARGETS` map to find the Pages deployment target +4. Worker rewrites the hostname and fetches from Pages (e.g. `marketdata-docs.pages.dev/docs/api/stocks`) +5. Pages serves the file from its `docs/` directory (built and nested there by CI) +6. Worker returns the response — the URL path stays the same throughout + +### Environments + +| Environment | Hostname | Pages Project | Git Branch | +|-------------|----------|---------------|------------| +| Production | `www.marketdata.app` | `marketdata-docs` | `main` | +| Staging | `www-staging.marketdata.app` | `marketdata-docs-staging` | `staging` | + +### Worker features + +The Worker (`worker/handler.js`) handles more than just proxying: + +- **Markdown serving** — Requests with `.md` extension or `Accept: text/markdown` header return cleaned markdown fetched from the raw GitHub source (frontmatter and JSX stripped) +- **SDK PHP redirect** — `/docs/sdk-php/*` redirects to GitHub Pages (301) +- **Edge caching** — Subrequests use `cf.cacheEverything` +- **404 logging** — Logs pathname and referer for missing pages +- Non-docs paths pass through to the origin (WordPress) + ## Deployment -The site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse proxy. Deployment is fully automated via GitHub Actions. +Deployment is fully automated via GitHub Actions (`.github/workflows/deploy-docs.yml`). -1. Push to `staging` — deploys to the staging site -2. Verify changes at the staging URL +1. Push to `staging` — builds and deploys to the staging Pages project +2. Verify changes at `www-staging.marketdata.app/docs/` 3. Open a PR from `staging` → `main` and merge — deploys to production -The CI pipeline (`.github/workflows/deploy-docs.yml`) builds the Docusaurus site, restructures the output to nest under `/docs/`, generates cache headers, and deploys via Wrangler. +The CI pipeline builds Docusaurus, restructures the output to nest under `docs/`, generates cache headers, uploads to R2, and deploys to Cloudflare Pages. If files in `worker/` changed, it also runs worker tests and deploys the Worker. + +## Testing + +```bash +# Worker unit tests +cd worker && yarn test + +# Integration tests (markdown serving against live site) +cd worker && TEST_ENV=staging yarn test:integration + +# E2E tests (Playwright — Context7 widget) +TEST_ENV=staging yarn test:e2e +``` ## Project Structure @@ -43,6 +90,7 @@ src/ theme/ # Swizzled Docusaurus theme components css/ # Custom styles worker/ # Cloudflare Worker reverse proxy +e2e/ # Playwright end-to-end tests .github/workflows # CI/CD pipeline ``` From 277dcd9a5e7b1f4f23057d78e692e45c833fdd60 Mon Sep 17 00:00:00 2001 From: MarketDataApp Date: Wed, 25 Feb 2026 16:39:04 -0300 Subject: [PATCH 3/4] refactor: migrate to unified CF Pages deployment via orchestrator - Update Worker TARGETS to new Pages projects (www-marketdata-app, www-staging-marketdata-app) - Switch CI to upload R2 artifacts at {env}/sources/docs/ and trigger orchestrator - Remove direct CF Pages deploy step and post-deploy-tests job (orchestrator handles both) - Update CLAUDE.md and README.md with new architecture --- .github/workflows/deploy-docs.yml | 73 +++++++------------------------ CLAUDE.md | 25 +++++++---- README.md | 22 +++++++--- worker/handler.js | 8 ++-- worker/handler.test.js | 6 +-- 5 files changed, 54 insertions(+), 80 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index aee59b38..f7c171f4 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,4 @@ -name: Deploy Docs to Cloudflare Pages +name: Deploy Docs on: push: @@ -9,7 +9,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - deployments: write steps: - uses: actions/checkout@v4 with: @@ -30,11 +29,9 @@ jobs: echo "base_url=docs" >> "$GITHUB_OUTPUT" if [ "$GIT_REF" = "refs/heads/main" ]; then echo "prod=true" >> "$GITHUB_OUTPUT" - echo "project=marketdata-docs" >> "$GITHUB_OUTPUT" echo "environment=production" >> "$GITHUB_OUTPUT" else echo "prod=" >> "$GITHUB_OUTPUT" - echo "project=marketdata-docs-staging" >> "$GITHUB_OUTPUT" echo "environment=staging" >> "$GITHUB_OUTPUT" fi @@ -74,34 +71,25 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} AWS_ENDPOINT_URL: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com AWS_REGION: auto + ENVIRONMENT: ${{ steps.env.outputs.environment }} run: | aws s3 sync build/ \ - "s3://www-marketdata-app-builds/${{ steps.env.outputs.environment }}/" \ + "s3://www-marketdata-app-builds/${ENVIRONMENT}/sources/docs/" \ --delete - # - name: Notify orchestrator - # if: success() - # uses: peter-evans/repository-dispatch@v3 - # with: - # token: ${{ secrets.ORCHESTRATOR_PAT }} - # repository: MarketDataApp/www-marketdata-app - # event-type: site-built - # client-payload: >- - # { - # "source": "docs", - # "environment": "${{ steps.env.outputs.environment }}", - # "commit_sha": "${{ github.sha }}" - # } - - - name: Deploy to Cloudflare Pages - uses: cloudflare/wrangler-action@v3 + - name: Notify orchestrator + if: success() + uses: peter-evans/repository-dispatch@v3 with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: >- - pages deploy build - --project-name=${{ steps.env.outputs.project }} - --commit-dirty=true + token: ${{ secrets.ORCHESTRATOR_PAT }} + repository: MarketDataApp/www-marketdata-app + event-type: site-built + client-payload: >- + { + "source": "docs", + "environment": "${{ steps.env.outputs.environment }}", + "commit_sha": "${{ github.sha }}" + } - name: Check if Worker changed id: worker @@ -127,34 +115,3 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy workingDirectory: worker - - post-deploy-tests: - needs: deploy - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: yarn - - - run: yarn install --frozen-lockfile - - - name: Install worker test dependencies - working-directory: worker - run: yarn install --frozen-lockfile - - - name: Run integration tests against production - working-directory: worker - env: - TEST_ENV: production - run: yarn test:integration - - - run: npx playwright install chromium - - - name: Run e2e tests against production - env: - TEST_ENV: production - run: yarn test:e2e diff --git a/CLAUDE.md b/CLAUDE.md index d282a323..467866b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,8 @@ | Environment | URL | Pages Project | Branch | |-------------|-----|---------------|--------| -| Production | `www.marketdata.app/docs/` | `marketdata-docs` | `main` | -| Staging | `www-staging.marketdata.app/docs/` | `marketdata-docs-staging` | `staging` | +| Production | `www.marketdata.app/docs/` | `www-marketdata-app` | `main` | +| Staging | `www-staging.marketdata.app/docs/` | `www-staging-marketdata-app` | `staging` | ## Architecture @@ -17,7 +17,7 @@ 1. DNS resolves the hostname (both are proxied CNAMEs in Cloudflare) 2. Cloudflare routes `/docs` and `/docs/*` to the Worker (via `wrangler.toml` route patterns) 3. Worker (`worker/handler.js`) looks up the hostname in the `TARGETS` map to find the Pages target -4. Worker rewrites the hostname and fetches from the Pages project (e.g. `marketdata-docs.pages.dev/docs/api/stocks`) +4. Worker rewrites the hostname and fetches from the Pages project (e.g. `www-marketdata-app.pages.dev/docs/api/stocks`) 5. Pages serves the file from its `docs/` directory (built and nested there by CI) 6. Worker returns the response to the client — path stays the same throughout @@ -31,18 +31,27 @@ - **404 logging**: Logs pathname and referer for 404 responses - Non-docs paths pass through to the origin (WordPress) -### CI/CD pipeline (`.github/workflows/deploy-docs.yml`) +### CI/CD pipeline + +**Docs repo** (`.github/workflows/deploy-docs.yml`): 1. Builds Docusaurus (`yarn build`) -2. Restructures build output to nest under `build/docs/` (both environments use same structure) +2. Restructures build output to nest under `build/docs/` 3. Generates `_headers` file for asset cache control -4. Uploads build to R2 (`www-marketdata-app-builds` bucket) -5. Deploys to Cloudflare Pages via Wrangler +4. Uploads build to R2 (`www-marketdata-app-builds` bucket) at `{env}/sources/docs/` +5. Triggers orchestrator via `repository_dispatch` 6. If `worker/` files changed: runs worker tests, then deploys the worker +**Orchestrator** (`MarketDataApp/www-marketdata-app`, `.github/workflows/deploy-site.yml`): + +1. Downloads all sources from R2 (`{env}/sources/`) +2. Merges into unified `build/` directory +3. Deploys to Cloudflare Pages (`www-marketdata-app` or `www-staging-marketdata-app`) +4. Runs post-deploy integration and e2e tests + ### DNS -- `www-staging.marketdata.app` → CNAME to `marketdata-docs-staging.pages.dev` (proxied) +- `www-staging.marketdata.app` → CNAME to `www-staging-marketdata-app.pages.dev` (proxied) - `www.marketdata.app` → existing DNS (proxied) ## Workflow diff --git a/README.md b/README.md index f8f428a1..5398f9ba 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ yarn build # Production build ## Architecture -The site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse proxy. Both production and staging use the same `/docs/` base path — routing is determined by hostname, not path prefix. +The site is hosted on **Cloudflare Pages** with a **Cloudflare Worker** reverse proxy. Both production and staging use the same `/docs/` base path — routing is determined by hostname, not path prefix. Deployment is handled by a separate orchestrator repo (`MarketDataApp/www-marketdata-app`) that merges build artifacts from R2 and deploys to unified Pages projects. ### Request flow @@ -35,7 +35,7 @@ Browser → Cloudflare DNS → Worker (hostname lookup) → Cloudflare Pages → 1. DNS resolves the hostname (both are proxied CNAMEs in Cloudflare) 2. Cloudflare routes `/docs` and `/docs/*` to the Worker (via `wrangler.toml` route patterns) 3. Worker looks up the hostname in a `TARGETS` map to find the Pages deployment target -4. Worker rewrites the hostname and fetches from Pages (e.g. `marketdata-docs.pages.dev/docs/api/stocks`) +4. Worker rewrites the hostname and fetches from Pages (e.g. `www-marketdata-app.pages.dev/docs/api/stocks`) 5. Pages serves the file from its `docs/` directory (built and nested there by CI) 6. Worker returns the response — the URL path stays the same throughout @@ -43,8 +43,8 @@ Browser → Cloudflare DNS → Worker (hostname lookup) → Cloudflare Pages → | Environment | Hostname | Pages Project | Git Branch | |-------------|----------|---------------|------------| -| Production | `www.marketdata.app` | `marketdata-docs` | `main` | -| Staging | `www-staging.marketdata.app` | `marketdata-docs-staging` | `staging` | +| Production | `www.marketdata.app` | `www-marketdata-app` | `main` | +| Staging | `www-staging.marketdata.app` | `www-staging-marketdata-app` | `staging` | ### Worker features @@ -58,13 +58,21 @@ The Worker (`worker/handler.js`) handles more than just proxying: ## Deployment -Deployment is fully automated via GitHub Actions (`.github/workflows/deploy-docs.yml`). +Deployment is fully automated via GitHub Actions across two repos: -1. Push to `staging` — builds and deploys to the staging Pages project +1. **This repo** (`.github/workflows/deploy-docs.yml`) — builds Docusaurus, uploads to R2, triggers orchestrator +2. **Orchestrator** (`MarketDataApp/www-marketdata-app`) — downloads all sources from R2, merges into unified build, deploys to CF Pages, runs post-deploy tests + +``` +Push to staging/main → Build → Upload to R2 → Trigger orchestrator → Deploy to CF Pages → Tests +``` + +Workflow: +1. Push to `staging` — builds and deploys to staging 2. Verify changes at `www-staging.marketdata.app/docs/` 3. Open a PR from `staging` → `main` and merge — deploys to production -The CI pipeline builds Docusaurus, restructures the output to nest under `docs/`, generates cache headers, uploads to R2, and deploys to Cloudflare Pages. If files in `worker/` changed, it also runs worker tests and deploys the Worker. +If files in `worker/` changed, the docs CI also runs worker tests and deploys the Worker. ## Testing diff --git a/worker/handler.js b/worker/handler.js index 775b8872..60d747a4 100644 --- a/worker/handler.js +++ b/worker/handler.js @@ -3,8 +3,8 @@ * and www-staging.marketdata.app to Cloudflare Pages deployments. * * Routing: - * - www.marketdata.app/docs/* → marketdata-docs.pages.dev/docs/* - * - www-staging.marketdata.app/docs/* → marketdata-docs-staging.pages.dev/docs/* + * - www.marketdata.app/docs/* → www-marketdata-app.pages.dev/docs/* + * - www-staging.marketdata.app/docs/* → www-staging-marketdata-app.pages.dev/docs/* * * The path is preserved as-is (including the /docs/ prefix). * Cloudflare Pages serves files from a matching directory structure in the @@ -18,8 +18,8 @@ * Hostname-based routing: maps each hostname to its Cloudflare Pages target. */ const TARGETS = { - 'www.marketdata.app': 'marketdata-docs.pages.dev', - 'www-staging.marketdata.app': 'marketdata-docs-staging.pages.dev', + 'www.marketdata.app': 'www-marketdata-app.pages.dev', + 'www-staging.marketdata.app': 'www-staging-marketdata-app.pages.dev', }; /** diff --git a/worker/handler.test.js b/worker/handler.test.js index 1880d116..5c7cd9eb 100644 --- a/worker/handler.test.js +++ b/worker/handler.test.js @@ -251,7 +251,7 @@ describe('handleRequest', () => { const req = makeRequest('https://www.marketdata.app/docs/api/stocks'); await handleRequest(req); const calledWith = mockFetch.mock.calls[0][0]; - expect(new URL(calledWith.url).hostname).toBe('marketdata-docs.pages.dev'); + expect(new URL(calledWith.url).hostname).toBe('www-marketdata-app.pages.dev'); }); it('proxies /docs/* on www-staging.marketdata.app to staging Pages', async () => { @@ -259,7 +259,7 @@ describe('handleRequest', () => { const req = makeRequest('https://www-staging.marketdata.app/docs/api/stocks'); await handleRequest(req); const calledWith = mockFetch.mock.calls[0][0]; - expect(new URL(calledWith.url).hostname).toBe('marketdata-docs-staging.pages.dev'); + expect(new URL(calledWith.url).hostname).toBe('www-staging-marketdata-app.pages.dev'); }); it('preserves the full path when proxying', async () => { @@ -283,7 +283,7 @@ describe('handleRequest', () => { const req = makeRequest('https://www.marketdata.app/docs'); await handleRequest(req); const calledWith = mockFetch.mock.calls[0][0]; - expect(new URL(calledWith.url).hostname).toBe('marketdata-docs.pages.dev'); + expect(new URL(calledWith.url).hostname).toBe('www-marketdata-app.pages.dev'); }); }); From 976d9e7f6a12d30f5d7a937318008ebf57b8e984 Mon Sep 17 00:00:00 2001 From: MarketDataApp Date: Wed, 25 Feb 2026 16:49:37 -0300 Subject: [PATCH 4/4] ci: add post-deploy-tests workflow triggered by orchestrator dispatch --- .github/workflows/post-deploy-tests.yml | 37 +++++++++++++++++++++++++ CLAUDE.md | 8 +++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/post-deploy-tests.yml diff --git a/.github/workflows/post-deploy-tests.yml b/.github/workflows/post-deploy-tests.yml new file mode 100644 index 00000000..f657621e --- /dev/null +++ b/.github/workflows/post-deploy-tests.yml @@ -0,0 +1,37 @@ +name: Post-Deploy Tests + +on: + repository_dispatch: + types: [deploy-complete] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.client_payload.commit_sha }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: yarn + + - run: yarn install --frozen-lockfile + + - name: Install worker test dependencies + working-directory: worker + run: yarn install --frozen-lockfile + + - name: Run integration tests + working-directory: worker + env: + TEST_ENV: ${{ github.event.client_payload.environment }} + run: yarn test:integration + + - run: npx playwright install chromium + + - name: Run e2e tests + env: + TEST_ENV: ${{ github.event.client_payload.environment }} + run: yarn test:e2e diff --git a/CLAUDE.md b/CLAUDE.md index 467866b1..97952736 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,13 @@ 1. Downloads all sources from R2 (`{env}/sources/`) 2. Merges into unified `build/` directory 3. Deploys to Cloudflare Pages (`www-marketdata-app` or `www-staging-marketdata-app`) -4. Runs post-deploy integration and e2e tests +4. Notifies source repo via `deploy-complete` dispatch + +**Post-deploy tests** (`.github/workflows/post-deploy-tests.yml`): + +1. Triggered by `deploy-complete` from the orchestrator +2. Checks out the docs repo at the deployed commit SHA +3. Runs integration tests and e2e tests against the deployed environment ### DNS