Docs Live Smoke #878
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docs Live Smoke | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: docs-live-smoke-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| smoke: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Smoke live docs pages | |
| env: | |
| BASE_URL: https://documentation.openclaw.ai | |
| GITHUB_SHA: ${{ github.sha }} | |
| PAGES: | | |
| /tools/reactions | |
| /zh-CN/tools/reactions | |
| /de/tools/reactions | |
| /de/gateway/heartbeat | |
| run: | | |
| node - <<'NODE' | |
| const baseUrl = process.env.BASE_URL; | |
| const pages = (process.env.PAGES ?? "") | |
| .split(/\r?\n/) | |
| .map((line) => line.trim()) | |
| .filter(Boolean); | |
| const poison = [ | |
| /\banalysis\s+to=functions\./iu, | |
| /\b(?:commentary|final)\s+to=functions\./iu, | |
| /\bfunctions\.(?:read|write|exec|search|run)\b/iu, | |
| /\b[A-Za-z_\u3400-\u9fff][\w\u3400-\u9fff-]*_input=\{/u, | |
| /<\/?openclaw_docs_i18n_input>/iu, | |
| /\/home\/runner\/work\//u, | |
| /彩神马争霸/u, | |
| ]; | |
| const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| const titleOf = (html) => { | |
| const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i); | |
| return match ? match[1].replace(/\s+/g, " ").trim() : ""; | |
| }; | |
| const decode = (value) => | |
| value | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll(""", '"') | |
| .replaceAll("'", "'") | |
| .replaceAll("'", "'"); | |
| const assertPage = async (path, attempt) => { | |
| const url = new URL(path, baseUrl); | |
| url.searchParams.set("_openclaw_smoke", `${process.env.GITHUB_SHA}-${attempt}`); | |
| const response = await fetch(url, { | |
| headers: { | |
| "cache-control": "no-cache", | |
| pragma: "no-cache", | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`${path}: HTTP ${response.status}`); | |
| } | |
| const html = await response.text(); | |
| const title = decode(titleOf(html)); | |
| if (!title || title.includes("404") || title.includes("Not Found")) { | |
| throw new Error(`${path}: bad title ${JSON.stringify(title)}`); | |
| } | |
| for (const pattern of poison) { | |
| if (pattern.test(html) || pattern.test(title)) { | |
| throw new Error(`${path}: poison text matched ${pattern}`); | |
| } | |
| } | |
| console.log(`${path}: ok (${title})`); | |
| }; | |
| const assertMarkdown = async (path) => { | |
| const url = new URL(path, baseUrl); | |
| const response = await fetch(url, { | |
| headers: { | |
| accept: "text/markdown", | |
| "cache-control": "no-cache", | |
| pragma: "no-cache", | |
| }, | |
| }); | |
| if (!response.ok) throw new Error(`${path}: markdown HTTP ${response.status}`); | |
| const text = await response.text(); | |
| const contentType = response.headers.get("content-type") ?? ""; | |
| if (!contentType.includes("text/markdown")) { | |
| throw new Error(`${path}: expected markdown content-type, got ${contentType}`); | |
| } | |
| if (!/^---\n/m.test(text)) throw new Error(`${path}: markdown frontmatter missing`); | |
| console.log(`${path}: ok (markdown)`); | |
| }; | |
| const deadline = Date.now() + 20 * 60 * 1000; | |
| let lastError; | |
| for (let attempt = 1; Date.now() < deadline; attempt += 1) { | |
| try { | |
| for (const page of pages) { | |
| await assertPage(page, attempt); | |
| } | |
| await assertMarkdown("/concepts/models"); | |
| process.exit(0); | |
| } catch (error) { | |
| lastError = error; | |
| console.log(`Attempt ${attempt} failed: ${error.message}`); | |
| await sleep(Math.min(15_000 * attempt, 60_000)); | |
| } | |
| } | |
| throw lastError ?? new Error("Docs live smoke timed out."); | |
| NODE |