153 lines
6.0 KiB
YAML
153 lines
6.0 KiB
YAML
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 assertText = async (path, pattern) => {
|
|
const url = new URL(path, baseUrl);
|
|
url.searchParams.set("_openclaw_smoke", process.env.GITHUB_SHA);
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
"cache-control": "no-cache",
|
|
pragma: "no-cache",
|
|
},
|
|
});
|
|
if (!response.ok) throw new Error(`${path}: text HTTP ${response.status}`);
|
|
const text = await response.text();
|
|
const contentType = response.headers.get("content-type") ?? "";
|
|
if (!/text\/plain|text\/markdown|application\/xml/.test(contentType)) {
|
|
throw new Error(`${path}: unexpected content-type ${contentType}`);
|
|
}
|
|
if (!pattern.test(text)) throw new Error(`${path}: expected marker missing`);
|
|
console.log(`${path}: ok (${contentType})`);
|
|
};
|
|
const assertStatus = async (path, status) => {
|
|
const response = await fetch(new URL(path, baseUrl), {
|
|
headers: {
|
|
"cache-control": "no-cache",
|
|
pragma: "no-cache",
|
|
},
|
|
});
|
|
if (response.status !== status) throw new Error(`${path}: expected HTTP ${status}, got ${response.status}`);
|
|
console.log(`${path}: ok (HTTP ${status})`);
|
|
};
|
|
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");
|
|
await assertText("/robots.txt", /Sitemap: .*\/sitemap\.xml/);
|
|
await assertText("/llms.txt", /## Documentation Index/);
|
|
await assertText("/.well-known/llms.txt", /## Documentation Index/);
|
|
await assertText("/sitemap.xml", /<urlset/);
|
|
await assertStatus("/llms-full.txt", 410);
|
|
await assertStatus("/.well-known/llms-full.txt", 410);
|
|
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
|