mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(landing): short HTML edge TTL + host purge after production deploy (#6552)
* fix(landing): shorten HTML edge TTL and host-purge after production deploy Long s-maxage=3600 + stale-while-revalidate=86400 left locale HTML (/zh/pricing/, /de/pricing/, …) on different edge objects after landing-page-production, so some languages looked deployed and others did not. Cap HTML/edge docs at s-maxage=60 without multi-hour SWR, and purge open-design.ai by hostname after pages deploy (token needs Zone Cache Purge). Hashed _astro assets stay immutable. * ci(landing): add manual host-purge workflow for edge cache recovery Allows testing CLOUDFLARE_API_TOKEN Cache Purge permission and one-click recovery without a full production rebuild. Production deploy still purges after pages deploy. * ci(landing): support purge-only production workflow mode Lets us exercise CLOUDFLARE_API_TOKEN Cache Purge without a full rebuild, and keeps host purge as a post-deploy job after a real promote. * ci(landing): move edge-cache purge helper to workflow-owned TypeScript pnpm guard rejects project-owned .mjs, so the new purge helper blocked Validate workspace. Convert it to TypeScript and move it next to the other landing workflow helpers under .github/scripts/, invoked with --experimental-strip-types like landing-page-deploy-feishu.ts. Also drop the purge-only production mode: blog-indexing-on-deploy listens on successful landing-page-production runs and cannot tell a cache-only recovery run from a real deploy. Purging without a rebuild already has a dedicated entry point in landing-edge-cache-purge, so production purge now runs only after a successful deploy.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Purge Cloudflare edge cache for the marketing host(s) by hostname.
|
||||
//
|
||||
// Used by `landing-page-production` after `pages deploy` so locale HTML
|
||||
// (`/`, `/zh/pricing/`, …) does not keep serving pre-deploy objects when
|
||||
// CF Pages' deploy invalidation is incomplete for the custom domain, and by
|
||||
// `landing-edge-cache-purge` for manual incident recovery / token checks.
|
||||
//
|
||||
// Inputs (all via env):
|
||||
// CLOUDFLARE_API_TOKEN (required) token with Zone → Cache Purge on the zone
|
||||
// CLOUDFLARE_ZONE_ID (optional) defaults to the open-design.ai zone id
|
||||
// CLOUDFLARE_PURGE_HOSTS (optional) comma-separated hostnames, default
|
||||
// `open-design.ai`
|
||||
//
|
||||
// Scope: hosts purge is intentional — one host, all paths/locales, without
|
||||
// touching other hostnames on the same zone (e.g. download.open-design.ai).
|
||||
//
|
||||
// @see https://developers.cloudflare.com/api/resources/cache/methods/purge/
|
||||
// @see https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-hostname/
|
||||
|
||||
type PurgeResponse = {
|
||||
success?: boolean;
|
||||
errors?: unknown;
|
||||
result?: unknown;
|
||||
};
|
||||
|
||||
const DEFAULT_ZONE_ID = "84ed4658186179c7eba52659b6ef48ad";
|
||||
|
||||
const token = process.env.CLOUDFLARE_API_TOKEN?.trim();
|
||||
const zoneId = process.env.CLOUDFLARE_ZONE_ID?.trim() || DEFAULT_ZONE_ID;
|
||||
const hosts = (process.env.CLOUDFLARE_PURGE_HOSTS || "open-design.ai")
|
||||
.split(",")
|
||||
.map((host) => host.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!token) {
|
||||
console.error("CLOUDFLARE_API_TOKEN is required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (hosts.length === 0) {
|
||||
console.error("CLOUDFLARE_PURGE_HOSTS resolved to an empty host list");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ hosts }),
|
||||
},
|
||||
);
|
||||
|
||||
const payload = (await response
|
||||
.json()
|
||||
.catch(() => null)) as PurgeResponse | null;
|
||||
const ok = response.ok && payload?.success === true;
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
httpStatus: response.status,
|
||||
zoneId,
|
||||
hosts,
|
||||
success: ok,
|
||||
errors: payload?.errors ?? null,
|
||||
result: payload?.result ?? null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
console.error(
|
||||
`Cloudflare host purge failed. Ensure CLOUDFLARE_API_TOKEN has Zone.Cache Purge on ${hosts.join(", ")}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
name: landing-edge-cache-purge
|
||||
|
||||
# Manual / test entry to host-purge open-design.ai (or staging) without a full
|
||||
# production rebuild. Production deploy also purges after pages deploy; this
|
||||
# workflow is for incident recovery and permission checks.
|
||||
#
|
||||
# Requires secrets.CLOUDFLARE_API_TOKEN with Zone → Cache Purge on the target
|
||||
# zone. Optional: vars.CLOUDFLARE_ZONE_ID.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
hosts:
|
||||
description: 'Comma-separated hostnames to purge (no scheme)'
|
||||
required: true
|
||||
default: 'open-design.ai'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
purge:
|
||||
name: Purge Cloudflare edge cache by hostname
|
||||
if: github.repository == 'nexu-io/open-design'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Purge hosts
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ZONE_ID: ${{ vars.CLOUDFLARE_ZONE_ID }}
|
||||
CLOUDFLARE_PURGE_HOSTS: ${{ inputs.hosts }}
|
||||
run: node --experimental-strip-types .github/scripts/landing-page-purge-edge-cache.ts
|
||||
@@ -35,7 +35,9 @@ jobs:
|
||||
# the main ref so a dispatch from a feature branch/tag is skipped outright
|
||||
# (no deploy) instead of recording a non-main production run — which would
|
||||
# also dodge blog-indexing's `workflow_run` `branches: [main]` filter.
|
||||
if: github.repository == 'nexu-io/open-design' && github.ref == 'refs/heads/main'
|
||||
if: >
|
||||
github.repository == 'nexu-io/open-design'
|
||||
&& github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
environment:
|
||||
@@ -205,6 +207,37 @@ jobs:
|
||||
--project-name=open-design-landing
|
||||
--branch=main
|
||||
|
||||
# CF Pages deploy does not reliably invalidate custom-domain HTML across every
|
||||
# POP when `_headers` allows edge caching. Purge by hostname so all locale
|
||||
# paths (`/`, `/zh/pricing/`, …) drop together without purge_everything on
|
||||
# the whole zone (keeps download.open-design.ai etc.).
|
||||
# Token needs Zone → Cache Purge. Optional: set vars.CLOUDFLARE_ZONE_ID.
|
||||
# Only runs after a real deploy. Purging without a rebuild (incident recovery,
|
||||
# token permission check) belongs to the dedicated `landing-edge-cache-purge`
|
||||
# workflow, so a cache-only run never reaches this workflow's `workflow_run`
|
||||
# consumers (notably `blog-indexing-on-deploy`).
|
||||
purge:
|
||||
name: Purge open-design.ai edge cache (hosts)
|
||||
needs: deploy
|
||||
if: github.repository == 'nexu-io/open-design'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Purge open-design.ai edge cache (hosts)
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ZONE_ID: ${{ vars.CLOUDFLARE_ZONE_ID }}
|
||||
CLOUDFLARE_PURGE_HOSTS: open-design.ai
|
||||
run: node --experimental-strip-types .github/scripts/landing-page-purge-edge-cache.ts
|
||||
|
||||
# Post a Feishu card to the landing group once the promotion settles. Runs on
|
||||
# BOTH success and failure (a failed production deploy is exactly when people
|
||||
# need a ping), but only when `deploy` actually ran — a dispatch from a
|
||||
|
||||
Reference in New Issue
Block a user