Docs Agent #146
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 Agent | |
| on: | |
| workflow_run: # zizmor: ignore[dangerous-triggers] main-only docs repair after trusted CI; job gates repository, event, branch, actor, conclusion, exact current main SHA, and hourly cadence before using write token | |
| workflows: | |
| - CI | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| permissions: | |
| actions: read | |
| contents: write | |
| concurrency: | |
| group: docs-agent-main | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| update-docs: | |
| if: > | |
| github.repository == 'openclaw/openclaw' && | |
| github.actor != 'github-actions[bot]' && | |
| (github.event_name != 'workflow_run' || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'push' && | |
| github.event.workflow_run.head_branch == 'main' && | |
| github.event.workflow_run.actor.login != 'github-actions[bot]')) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| submodules: false | |
| - name: Gate trusted main activity and hourly cadence | |
| id: gate | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| GH_TOKEN: ${{ github.token }} | |
| WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" != "workflow_run" ]; then | |
| head_sha="$(git rev-parse HEAD)" | |
| review_base="$(git rev-parse "${head_sha}^" 2>/dev/null || printf '%s' "$head_sha")" | |
| { | |
| echo "run_agent=true" | |
| echo "base_sha=${head_sha}" | |
| echo "review_base_sha=${review_base}" | |
| echo "review_head_sha=${head_sha}" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| for attempt in 1 2 3 4 5; do | |
| if git fetch --no-tags origin main; then | |
| break | |
| fi | |
| if [ "$attempt" = "5" ]; then | |
| echo "Failed to fetch main after retries." >&2 | |
| exit 1 | |
| fi | |
| echo "Fetch attempt ${attempt} failed; retrying." | |
| sleep $((attempt * 2)) | |
| done | |
| remote_main="$(git rev-parse origin/main)" | |
| if [ "$remote_main" != "$WORKFLOW_HEAD_SHA" ]; then | |
| echo "CI run is superseded by ${remote_main}; skipping docs agent for ${WORKFLOW_HEAD_SHA}." | |
| echo "run_agent=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| runs_json="$RUNNER_TEMP/docs-agent-runs.json" | |
| gh api --method GET "repos/${GITHUB_REPOSITORY}/actions/workflows/docs-agent.yml/runs" \ | |
| -f branch=main \ | |
| -f event=workflow_run \ | |
| -f per_page=100 > "$runs_json" | |
| one_hour_ago="$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" | |
| recent_runs="$( | |
| jq -r \ | |
| --argjson current_run_id "$GITHUB_RUN_ID" \ | |
| --arg one_hour_ago "$one_hour_ago" \ | |
| '.workflow_runs[] | |
| | select(.database_id != $current_run_id) | |
| | select(.created_at >= $one_hour_ago) | |
| | select(.status != "cancelled") | |
| | select((.conclusion // "") != "skipped") | |
| | [.database_id, .status, (.conclusion // ""), .created_at, .head_sha] | |
| | @tsv' "$runs_json" | |
| )" | |
| if [ -n "$recent_runs" ]; then | |
| echo "Docs agent already ran or is running within the last hour; skipping." | |
| printf '%s\n' "$recent_runs" | |
| echo "run_agent=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| review_base="$( | |
| jq -r \ | |
| --argjson current_run_id "$GITHUB_RUN_ID" \ | |
| --arg remote_main "$remote_main" \ | |
| '.workflow_runs[] | |
| | select(.database_id != $current_run_id) | |
| | select(.status != "cancelled") | |
| | select((.conclusion // "") != "skipped") | |
| | .head_sha | |
| | select(. != null and . != "") | |
| | select(. != $remote_main) | |
| ' "$runs_json" | head -n 1 | |
| )" | |
| if [ -z "$review_base" ] || ! git cat-file -e "${review_base}^{commit}" 2>/dev/null; then | |
| review_base="$(git rev-parse "${remote_main}^" 2>/dev/null || printf '%s' "$remote_main")" | |
| fi | |
| { | |
| echo "run_agent=true" | |
| echo "base_sha=${remote_main}" | |
| echo "review_base_sha=${review_base}" | |
| echo "review_head_sha=${remote_main}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Setup Node environment | |
| if: steps.gate.outputs.run_agent == 'true' | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Ensure docs agent key exists | |
| if: steps.gate.outputs.run_agent == 'true' | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${OPENAI_API_KEY:-}" ]; then | |
| echo "Missing OPENCLAW_DOCS_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2 | |
| exit 1 | |
| fi | |
| - name: Run Codex docs agent | |
| if: steps.gate.outputs.run_agent == 'true' | |
| uses: openai/codex-action@v1 | |
| env: | |
| DOCS_AGENT_BASE_SHA: ${{ steps.gate.outputs.review_base_sha }} | |
| DOCS_AGENT_HEAD_SHA: ${{ steps.gate.outputs.review_head_sha }} | |
| with: | |
| openai-api-key: ${{ secrets.OPENCLAW_DOCS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} | |
| prompt-file: .github/codex/prompts/docs-agent.md | |
| model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }} | |
| effort: medium | |
| sandbox: workspace-write | |
| safety-strategy: drop-sudo | |
| codex-args: '["--full-auto"]' | |
| - name: Enforce existing-docs-only patch | |
| if: steps.gate.outputs.run_agent == 'true' | |
| run: | | |
| set -euo pipefail | |
| untracked="$(git ls-files --others --exclude-standard)" | |
| if [ -n "$untracked" ]; then | |
| echo "Docs agent created untracked files; forbidden:" | |
| printf '%s\n' "$untracked" | |
| exit 1 | |
| fi | |
| added_or_deleted="$(git diff --name-status --diff-filter=AD)" | |
| if [ -n "$added_or_deleted" ]; then | |
| echo "Docs agent added or deleted tracked files; forbidden:" | |
| printf '%s\n' "$added_or_deleted" | |
| exit 1 | |
| fi | |
| bad_paths="$( | |
| git diff --name-only | while IFS= read -r path; do | |
| case "$path" in | |
| docs/*|README.md|CHANGELOG.md) ;; | |
| *) printf '%s\n' "$path" ;; | |
| esac | |
| done | |
| )" | |
| if [ -n "$bad_paths" ]; then | |
| echo "Docs agent touched non-doc paths; forbidden:" | |
| printf '%s\n' "$bad_paths" | |
| exit 1 | |
| fi | |
| - name: Restore Node 24 path | |
| if: steps.gate.outputs.run_agent == 'true' | |
| run: | |
| | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job | |
| set -euo pipefail | |
| export PATH="${NODE_BIN}:${PATH}" | |
| echo "${NODE_BIN}" >> "$GITHUB_PATH" | |
| node -v | |
| corepack enable | |
| pnpm -v | |
| - name: Check docs | |
| if: steps.gate.outputs.run_agent == 'true' | |
| run: pnpm check:docs | |
| - name: Commit docs updates | |
| if: steps.gate.outputs.run_agent == 'true' | |
| env: | |
| BASE_SHA: ${{ steps.gate.outputs.base_sha }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| TARGET_BRANCH: main | |
| run: | | |
| set -euo pipefail | |
| if git diff --quiet; then | |
| echo "No docs changes." | |
| exit 0 | |
| fi | |
| git config user.name "openclaw-docs-agent[bot]" | |
| git config user.email "openclaw-docs-agent[bot]@users.noreply.github.com" | |
| git add docs README.md CHANGELOG.md | |
| git commit --no-verify -m "docs: refresh documentation" | |
| for attempt in 1 2 3 4 5; do | |
| if ! git fetch --no-tags origin "${TARGET_BRANCH}"; then | |
| echo "Fetch attempt ${attempt} failed; retrying." | |
| sleep $((attempt * 2)) | |
| continue | |
| fi | |
| if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:"${TARGET_BRANCH}"; then | |
| exit 0 | |
| fi | |
| remote_main="$(git rev-parse "origin/${TARGET_BRANCH}")" | |
| if [ "$remote_main" != "$BASE_SHA" ]; then | |
| echo "main advanced from ${BASE_SHA} to ${remote_main}; skipping stale docs update." | |
| exit 0 | |
| fi | |
| echo "Docs update attempt ${attempt} failed; retrying." | |
| sleep $((attempt * 2)) | |
| done | |
| echo "Failed to push docs updates after retries." >&2 | |
| exit 1 |