Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 144 additions & 1 deletion .github/workflows/docs-live-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,64 @@ name: Docs Live Smoke

on:
workflow_dispatch:
inputs:
commit_range:
description: "Optional <base>..<head> git range; docs pages added within it are asserted live in addition to the fixed probes."
type: string
required: false
default: ""
schedule:
- cron: "23 6 * * *"

permissions:
contents: read

concurrency:
group: docs-live-smoke-${{ github.ref }}
group: docs-live-smoke-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
smoke:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v7.0.1
with:
fetch-depth: 1

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22

- name: Derive newly added routes
id: added-routes
if: inputs.commit_range != ''
env:
COMMIT_RANGE: ${{ inputs.commit_range }}
run: |
set -euo pipefail
if [[ ! "${COMMIT_RANGE}" =~ ^[0-9a-f]{7,40}\.\.[0-9a-f]{7,40}$ ]]; then
echo "Invalid commit_range: expected <base>..<head> with 7-40 lowercase hex characters per commit." >&2
exit 1
fi
base="${COMMIT_RANGE%%..*}"
head="${COMMIT_RANGE##*..}"
git fetch --quiet --no-tags --depth=1 origin "${base}" "${head}"
git diff --name-only --no-renames --diff-filter=A "${base}" "${head}" -- docs | node scripts/docs-site/added-routes.mjs > /tmp/openclaw-added-routes.txt
echo "Newly added routes:"
if [ -s /tmp/openclaw-added-routes.txt ]; then
cat /tmp/openclaw-added-routes.txt
else
echo "none"
fi

- name: Smoke live docs pages
env:
BASE_URL: https://docs.openclaw.ai
GITHUB_SHA: ${{ github.sha }}
EXTRA_PAGES_FILE: /tmp/openclaw-added-routes.txt
PAGES: |
/tools/reactions
/plugins/google-meet
Expand All @@ -36,6 +73,19 @@ jobs:
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const fs = await import("node:fs");
const extraPagesFile = process.env.EXTRA_PAGES_FILE;
const fixedPageCount = pages.length;
if (extraPagesFile && fs.existsSync(extraPagesFile)) {
const extraPages = fs.readFileSync(extraPagesFile, "utf8")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
for (const page of extraPages) {
if (!pages.includes(page)) pages.push(page);
}
}
console.log(`Added ${pages.length - fixedPageCount} extra routes.`);
const poison = [
/\banalysis\s+to=functions\./iu,
/\b(?:commentary|final)\s+to=functions\./iu,
Expand Down Expand Up @@ -211,3 +261,96 @@ jobs:
}
throw lastError ?? new Error("Docs live smoke timed out.");
NODE

# docs_map.md covers English routes only; locale coverage comes from the per-dispatch added-route assertions.
sample:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- name: Check out main
uses: actions/checkout@v7.0.1
with:
ref: main
fetch-depth: 1

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22

- name: Sample live docs routes
env:
BASE_URL: https://docs.openclaw.ai
run: |
node - <<'NODE'
import fs from "node:fs";

const routes = [...new Set(fs.readFileSync("docs/docs_map.md", "utf8")
.split(/\r?\n/)
.map((line) => line.match(/^- Route: (\/\S*)$/)?.[1])
.filter(Boolean))].sort();
if (routes.length < 100) {
throw new Error(`Expected at least 100 docs_map.md routes, parsed ${routes.length}.`);
}
const runId = process.env.GITHUB_RUN_ID;
let seed = Number(BigInt(runId) & 0xffffffffn);
for (let index = routes.length - 1; index > 0; index -= 1) {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
const other = Math.floor(seed / 0x100000000 * (index + 1));
[routes[index], routes[other]] = [routes[other], routes[index]];
}
const pages = routes.slice(0, 25);
console.log(`Sampling ${pages.length} of ${routes.length} routes for run ${runId}:\n${pages.join("\n")}`);
const baseUrl = process.env.BASE_URL;
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("&amp;", "&")
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&quot;", '"')
.replaceAll("&#x27;", "'")
.replaceAll("&#39;", "'");
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",
},
signal: AbortSignal.timeout(15_000),
});
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)}`);
}
console.log(`${path}: ok (${title})`);
};
let pending = pages;
for (let attempt = 1; attempt <= 3; attempt += 1) {
const failures = [];
for (const page of pending) {
try {
await assertPage(page, attempt);
} catch (error) {
failures.push({ page, message: error.message });
console.log(`Attempt ${attempt} failed: ${error.message}`);
}
}
if (failures.length === 0) break;
if (attempt === 3) {
throw new Error(`Docs route sample failed:\n${failures.map(({ message }) => message).join("\n")}`);
}
pending = failures.map(({ page }) => page);
await sleep(5_000);
}
NODE
38 changes: 36 additions & 2 deletions .github/workflows/r2-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ on:
required: false
type: string
default: ""
smoke_commit_range:
description: "Optional <base>..<head> forwarded to docs-live-smoke for added-route assertions."
type: string
required: false
default: ""

permissions:
actions: write
Expand Down Expand Up @@ -221,6 +226,7 @@ jobs:
set -euo pipefail
git fetch --quiet origin main
latest="$(git rev-parse refs/remotes/origin/main)"
echo "latest=${latest}" >> "${GITHUB_OUTPUT}"
expected="${SCOPED_CONTENT_SHA:-${GITHUB_SHA}}"
if [ "${expected}" = "${latest}" ]; then
echo "stale=false" >> "${GITHUB_OUTPUT}"
Expand Down Expand Up @@ -254,7 +260,23 @@ jobs:
if: steps.current-main.outputs.dispatch == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run r2-pages.yml --ref main -f artifact_scope=full -f request_id="head-drift-${GITHUB_RUN_ID}"
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before || '' }}
SMOKE_COMMIT_RANGE: ${{ inputs.smoke_commit_range || '' }}
LATEST_SHA: ${{ steps.current-main.outputs.latest }}
run: |
set -euo pipefail
base=""
if [[ "${EVENT_NAME}" == "push" && "${BEFORE_SHA}" =~ ^[0-9a-f]{7,40}$ && ! "${BEFORE_SHA}" =~ ^0+$ ]]; then
base="${BEFORE_SHA}"
elif [ -n "${SMOKE_COMMIT_RANGE}" ]; then
base="${SMOKE_COMMIT_RANGE%%..*}"
fi
range=""
if [ -n "${base}" ]; then
range="${base}..${LATEST_SHA}"
fi
gh workflow run r2-pages.yml --ref main -f artifact_scope=full -f request_id="head-drift-${GITHUB_RUN_ID}" -f smoke_commit_range="${range}"

- name: Fail stale scoped translation deploy
if: github.event_name == 'workflow_dispatch' && steps.current-main.outputs.stale == 'true' && (steps.artifact-scope.outputs.scope == 'locale' || steps.artifact-scope.outputs.scope == 'page')
Expand Down Expand Up @@ -394,4 +416,16 @@ jobs:
if: steps.current-main.outputs.stale == 'false' && (steps.artifact-scope.outputs.deploy_worker == '1' || (steps.artifact-scope.outputs.scope != 'none' && (steps.upload-r2.outputs.changed != '0' || steps.upload-r2.outputs.deleted != '0')))
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run docs-live-smoke.yml --ref main
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before || '' }}
AFTER_SHA: ${{ github.sha }}
SMOKE_COMMIT_RANGE: ${{ inputs.smoke_commit_range || '' }}
run: |
set -euo pipefail
range=""
if [[ "${EVENT_NAME}" == "push" && "${BEFORE_SHA}" =~ ^[0-9a-f]{7,40}$ && ! "${BEFORE_SHA}" =~ ^0+$ ]]; then
range="${BEFORE_SHA}..${AFTER_SHA}"
elif [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
range="${SMOKE_COMMIT_RANGE}"
fi
gh workflow run docs-live-smoke.yml --ref main -f commit_range="${range}"
37 changes: 37 additions & 0 deletions scripts/docs-site/added-routes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Map added docs files to live routes; mapping must mirror build.mjs page collection.
// The CLI caps large additions with a deterministic, evenly spaced sample.
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import { ignoredDocDirs, ignoredDocFiles } from "./config.mjs";

export function routesFromAddedDocsPaths(paths) {
const routes = new Set();
for (const path of paths) {
if (!path.startsWith("docs/")) continue;
const rel = path.slice("docs/".length);
if (!/\.(md|mdx)$/.test(rel) || ignoredDocFiles.has(rel)) continue;
const segments = rel.split("/");
if (segments.some((segment) => segment.startsWith(".") || ignoredDocDirs.has(segment))) continue;
if (["AGENTS.md", "CLAUDE.md"].includes(segments.at(-1))) continue;
const slug = rel.replace(/\.(md|mdx)$/, "").replace(/\/index$/, "");
routes.add(slug === "index" ? "/" : `/${slug}`);
}
return [...routes].sort();
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
const value = process.env.MAX_ROUTES ?? "100";
const cap = Number(value);
if (!/^\d+$/.test(value) || !Number.isSafeInteger(cap)) {
throw new Error("MAX_ROUTES must be a non-negative safe integer (0 = unlimited).");
}
let routes = routesFromAddedDocsPaths(fs.readFileSync(0, "utf8").split(/\r?\n/));
if (cap > 0 && routes.length > cap) {
const total = routes.length;
routes = Array.from({ length: cap }, (_, index) =>
routes[cap === 1 ? 0 : Math.round(index * (total - 1) / (cap - 1))]
);
console.error(`Sampled ${routes.length} routes out of ${total}.`);
}
if (routes.length > 0) process.stdout.write(`${routes.join("\n")}\n`);
}
Loading