From 3bac9d269bcf7d1bbc97c8a25973ba7c027daa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 15 Oct 2026 15:47:03 +0200 Subject: [PATCH 1/2] Proxy legacy pages from Cloudflare Pages instead of Render The legacy Jekyll site (docs, blog, and not-yet-ported pages) is now deployed at applandinc-github-io.pages.dev as part of migrating off Render onto Cloudflare entirely. Point the worker's legacy fallback origin there. Verified parity against the Render origin across the paths the worker proxies: identical 220-URL sitemap, byte-identical page bodies apart from build-time timestamps in the schema.org metadata, and a real 404 status on unknown paths, which fetchFromLegacySite relies on to fall through to the app's own 404. Pages adds trailing-slash and .html normalization that Render did not (/blog -> /blog/, /docs/ -> /docs, /foo.html -> /foo). Every one of those maps to the same string under normalizeLegacyPath, so the existing ping-pong guard follows them internally and serves content rather than bouncing the browser; no redirect is visible on appmap.io. Cost is one extra origin subrequest on those forms. The Pages project needed JEKYLL_ENV=production set before cutover: its first build omitted the Google Analytics and HubSpot embeds, which sit behind a jekyll.environment conditional. --- src/server.ts | 14 +++++++------- wrangler.jsonc | 8 +++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/server.ts b/src/server.ts index 6480d91..6bc4a8c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -20,12 +20,12 @@ async function getServerEntry(): Promise { return serverEntryPromise; } -// The legacy Jekyll site (docs, blog, and not-yet-ported pages) stays deployed -// on Render. Any GET/HEAD request this app 404s on is retried against that -// origin, so all pre-existing appmap.io URLs keep working unchanged. -// Preview deploys can override the origin (e.g. to a Render branch preview) +// The legacy Jekyll site (docs, blog, and not-yet-ported pages) is deployed on +// Cloudflare Pages. Any GET/HEAD request this app 404s on is retried against +// that origin, so all pre-existing appmap.io URLs keep working unchanged. +// Preview deploys can override the origin (e.g. to a Pages branch preview) // with a LEGACY_ORIGIN var; production uses the default. -const DEFAULT_LEGACY_ORIGIN = "https://appland-com.onrender.com"; +const DEFAULT_LEGACY_ORIGIN = "https://applandinc-github-io.pages.dev"; function legacyOrigin(env: unknown): string { // The nitro cloudflare entry does not pass `env` through to this handler @@ -142,8 +142,8 @@ function shouldTryLegacyFallback(request: Request, response: Response): boolean async function fetchFromLegacySite(request: Request, origin: string): Promise { const url = new URL(request.url); try { - // redirect: "manual" passes the legacy site's redirects (e.g. Render's - // .html-stripping 301s) through to the browser on our own domain. + // redirect: "manual" passes the legacy site's redirects (e.g. Pages' + // .html-stripping 308s) through to the browser on our own domain. const response = await fetch(new Request(origin + url.pathname + url.search, request), { redirect: "manual", }); diff --git a/wrangler.jsonc b/wrangler.jsonc index 6ef772b..52561e0 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -11,9 +11,11 @@ "vars": { "SUPABASE_URL": "https://ulrbnkrvxzbtqancxmtn.supabase.co", // Legacy docs/blog proxy origin: no LEGACY_ORIGIN var here means the - // worker uses its default, the Render build of applandinc.github.io - // master (see DEFAULT_LEGACY_ORIGIN in src/server.ts). PR #1583 merged - // 2026-09-04, so master is the post-cutover old site. + // worker uses its default, the Cloudflare Pages build of + // applandinc.github.io master (see DEFAULT_LEGACY_ORIGIN in + // src/server.ts). PR #1583 merged 2026-09-04, so master is the + // post-cutover old site. Render was retired as the legacy origin + // 2026-10-15. }, // Preview deploys use wrangler.preview.jsonc (a standalone config), NOT a // wrangler environment: nitro copies this file into the build output as a From 8337124bdc644a63eb654fe5a6cfc9fb820f5f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 15 Oct 2026 16:14:12 +0200 Subject: [PATCH 2/2] Deploy branch previews with Worker Previews, and fail when there is no URL The preview job has never produced a preview URL. It was not failing either: `wrangler versions upload` uploaded a version fine but printed no Version URL, the grep for one came up empty, and the "Add preview URL to summary" step was then skipped by its own `if:` guard. The job went green with nothing to preview. Version URLs were the wrong tool anyway. Worker Previews (GA 2026-09-22) give a branch a stable URL that every push updates, with isolated config, state and observability, where Version URLs only ever pointed at production resources. Switch the job to `wrangler preview` and add the `previews` block it requires. The block is empty for now; it is also where a per-Preview LEGACY_ORIGIN override would go if a Preview ever needs to point at a branch build of the legacy site. Unlike environments, "previews" is accepted in the redirected config nitro writes to .output/server/wrangler.json. The CLI still labels the command "private beta", but this account has access. Make the step fail outright when no URL comes back, and drop the `if:` guard that let an empty value silently skip the summary. A missing preview URL should be a red job, not a green one, however it comes about. The URL is read from --json, falling back to scraping the human output, since the JSON shape is not pinned down yet. Verified against the resulting Preview: the legacy docs and blog paths proxy from the Pages origin (confirmed by build timestamp), the .html and trailing-slash forms resolve to 200 without bouncing the browser, sitemap-docs.xml matches the Pages sitemap byte for byte, unknown paths still 404 to the app rather than the legacy site, and the redirect table is intact. Assisted-by: Claude:claude-opus-5 --- .github/workflows/deploy-preview.yml | 36 ++++++++++++++++++++++++---- wrangler.jsonc | 11 +++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 81b0c72..29c8e56 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -26,19 +26,47 @@ jobs: - name: Build site run: bun run build - - name: Upload preview version + # Worker Previews (`wrangler preview`, GA 2026-09-22) in place of + # `wrangler versions upload`. Previews give a branch a stable URL that + # every push updates, plus isolated config/state/observability, where + # Version URLs only ever pointed at production resources. + # The CLI still labels the command "private beta"; this account has + # access, so treat that label as stale rather than as a blocker. + # Preview names must be hostname-safe, so slashes in branch names are + # flattened rather than relying on the git-branch default. + - name: Deploy branch preview id: upload run: | set -o pipefail - bunx wrangler versions upload 2>&1 | tee upload_output.txt - PREVIEW_URL=$(grep -o 'https://[a-zA-Z0-9.-]*\.workers\.dev' upload_output.txt | head -1 || echo "") + PREVIEW_NAME=$(printf '%s' "$BRANCH" | tr -c 'a-zA-Z0-9' '-' | sed 's/--*/-/g; s/^-//; s/-$//') + echo "Preview name: $PREVIEW_NAME" + # --json is parsed first; the human output is kept for the log and + # used as a fallback, since the JSON shape is not pinned down yet. + bunx wrangler preview --name "$PREVIEW_NAME" --json > preview_output.json 2> preview_stderr.txt || STATUS=$? + cat preview_stderr.txt + cat preview_output.json + if [ -n "${STATUS:-}" ]; then + echo "::error::wrangler preview exited with status $STATUS" + exit "$STATUS" + fi + PREVIEW_URL=$(jq -r '[.. | strings | select(startswith("https://") and contains("workers.dev"))] | first // empty' preview_output.json 2>/dev/null || true) + if [ -z "$PREVIEW_URL" ]; then + PREVIEW_URL=$(grep -oh 'https://[a-zA-Z0-9.-]*\.workers\.dev' preview_output.json preview_stderr.txt | head -1 || true) + fi + if [ -z "$PREVIEW_URL" ]; then + echo "::error::wrangler preview produced no URL, so there is nothing to preview. See the output above for the shape it actually returned." + exit 1 + fi echo "PREVIEW_URL=$PREVIEW_URL" >> "$GITHUB_OUTPUT" env: + BRANCH: ${{ github.ref_name }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # No `if:` guard here on purpose: the upload step now fails outright when + # no URL is found, so an empty PREVIEW_URL can no longer skip this step + # and leave the job green with nothing to preview. - name: Add preview URL to summary - if: steps.upload.outputs.PREVIEW_URL run: | echo "## 🚀 Preview Deployment" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" diff --git a/wrangler.jsonc b/wrangler.jsonc index 52561e0..d33167c 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -8,6 +8,17 @@ "compatibility_flags": ["nodejs_compat"], "main": "src/server.ts", "routes": [{ "pattern": "appmap.io/*", "zone_name": "appmap.io" }], + // Paired with `wrangler preview` in deploy-preview.yml. Worker Previews + // require this block to exist; it may be empty when the Preview needs no + // settings of its own. If a Preview should ever point at a branch build of + // the legacy site rather than its production deploy, a + // "vars": { "LEGACY_ORIGIN": "..." } here is the hook for it (see + // legacyOrigin() in src/server.ts). + // + // Unlike environments, "previews" is accepted in the redirected config + // nitro writes to .output/server/wrangler.json — verified by a passing + // deploy-preview run, not assumed. + "previews": {}, "vars": { "SUPABASE_URL": "https://ulrbnkrvxzbtqancxmtn.supabase.co", // Legacy docs/blog proxy origin: no LEGACY_ORIGIN var here means the