281 uses: openai/codex-action@bdf19a4a223ec2549a3e2274a0cf61556bc07675 # v1.13282 env:283 DOCS_AGENT_BASE_SHA: ${{ steps.gate.outputs.review_base_sha }}284 DOCS_AGENT_HEAD_SHA: ${{ steps.gate.outputs.review_head_sha }}285 with:286 openai-api-key: ${{ secrets.OPENCLAW_DOCS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}287 prompt-file: .github/codex/prompts/docs-agent.md288 model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}289 effort: medium290 sandbox: workspace-write291 safety-strategy: drop-sudo292293 - name: Enforce existing-docs-only patch294 if: steps.gate.outputs.run_agent == 'true'295 run: |296 set -euo pipefail297298 untracked="$(python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 ls-files --others --exclude-standard)"299 if [ -n "$untracked" ]; then300 echo "Docs agent created untracked files; forbidden:"301 printf '%s\n' "$untracked"302 exit 1303 fi304305 added_or_deleted="$(306 python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 diff HEAD --name-status --diff-filter=AD || exit "$?"307 python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 diff --cached HEAD --name-status --diff-filter=AD || exit "$?"308 )"309 if [ -n "$added_or_deleted" ]; then310 echo "Docs agent added or deleted tracked files; forbidden:"311 printf '%s\n' "$added_or_deleted"312 exit 1313 fi314315 filter_bad_paths() {316 while IFS= read -r path; do317 case "$path" in318 docs/*|README.md) ;;319 CHANGELOG/*.md)320 # Only regenerate an existing mirror from its original ordered sources.321 if ! python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 show "HEAD:$path" |322 node --input-type=module -e '323 import { readFileSync } from "node:fs";324 import { changelogEntryPath } from "./scripts/lib/release-changelog.mjs";325 import { parseReleaseDocsMirror, renderReleaseDocsMirror } from "./scripts/lib/release-docs-mirror.mjs";326 const file = process.argv[1];327 const metadata = parseReleaseDocsMirror(readFileSync(0, "utf8"));328 if (!metadata || file !== changelogEntryPath(metadata.version) ||329 readFileSync(file, "utf8") !== renderReleaseDocsMirror({ rootDir: process.cwd(), ...metadata })) {330 throw new Error("Docs agent may only regenerate an existing docs mirror");331 }332 ' "$path"333 then334 printf '%s\n' "$path"335 fi336 ;;337 *) printf '%s\n' "$path" ;;338 esac339 done340 }341 # A restored working copy can still hide a forbidden edit in the index.342 bad_paths="$(343 {344 python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 diff HEAD --name-only || exit "$?"345 python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 diff --cached HEAD --name-only || exit "$?"346 } | sort -u | filter_bad_paths347 )"348 if [ -n "$bad_paths" ]; then349 echo "Docs agent touched non-doc paths; forbidden:"350 printf '%s\n' "$bad_paths"351 exit 1352 fi353354 - name: Restore Node 24 path355 if: steps.gate.outputs.run_agent == 'true'356 run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job357 set -euo pipefail358 export PATH="${NODE_BIN}:${PATH}"359 echo "${NODE_BIN}" >> "$GITHUB_PATH"360 node -v361 corepack enable362 pnpm -v363364 - name: Check docs365 if: steps.gate.outputs.run_agent == 'true'366 run: pnpm check:docs367368 - name: Commit docs updates369 if: steps.gate.outputs.run_agent == 'true'370 env:371 BASE_SHA: ${{ steps.gate.outputs.base_sha }}372 GITHUB_TOKEN: ${{ github.token }}373 TARGET_BRANCH: main374 run: |375 set -euo pipefail376377 exec python3 -I -S "$CI_GIT_OWNER" --policy - <<'PYTHON'378 import os379 import sys380 from ci_git_owner import run_git, git_output, GitFailure, FetchTimeout, backoff381382 workspace = os.getcwd()383 target = os.environ["TARGET_BRANCH"]384 base_sha = os.environ["BASE_SHA"]385 try:386 run_git(workspace, "diff", "HEAD", "--quiet")387 except GitFailure:388 pass389 else:390 print("No docs changes.")391 raise SystemExit(0)392393 run_git(workspace, "config", "user.name", "openclaw-docs-agent[bot]", reclaim_locks=True)394 run_git(workspace, "config", "user.email", "openclaw-docs-agent[bot]@users.noreply.github.com", reclaim_locks=True)395 run_git(workspace, "add", "docs", "README.md", "CHANGELOG", reclaim_locks=True)396 run_git(workspace, "commit", "--no-verify", "-m", "docs: refresh documentation", reclaim_locks=True)397 for attempt in range(1, 6):398 try:399 run_git(workspace, "fetch", "--no-tags", "origin", target, timeout=120, reclaim_locks=True)400 except (GitFailure, FetchTimeout):401 print(f"Fetch attempt {attempt} failed; retrying.", flush=True)402 backoff(attempt * 2)403 continue404 try:405 run_git(workspace, "push",406 f"https://x-access-token:{os.environ['GITHUB_TOKEN']}@github.com/{os.environ['GITHUB_REPOSITORY']}.git",407 f"HEAD:{target}", reclaim_locks=True)408 raise SystemExit(0)409 except GitFailure:410 # A push failure permits stale/retry decisions only after verified cleanup.411 remote_main = git_output(workspace, "rev-parse", f"origin/{target}").rstrip("\n")412 if remote_main != base_sha:413 print(f"main advanced from {base_sha} to {remote_main}; skipping stale docs update.")414 raise SystemExit(0)415 print(f"Docs update attempt {attempt} failed; retrying.", flush=True)416 backoff(attempt * 2)417 print("Failed to push docs updates after retries.", file=sys.stderr)418 raise SystemExit(1)419 PYTHON