automation | Router Docstring Sync #1
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: automation | Router Docstring Sync | |
| # Keeps route-handler docstrings in sync with what the handlers actually | |
| # accept. The published cognee_openapi_spec.json is generated straight from | |
| # the FastAPI app (tools/sync_release_docs.py -> app.openapi()), so these | |
| # docstrings are what keeps the spec's endpoint descriptions honest. | |
| # | |
| # When drift is detected, this workflow does not fail a build — it runs a | |
| # two-stage fix: the sync job corrects parameter drift mechanically | |
| # (tools/fix_router_docstrings.py) and opens/updates a PR against dev, then | |
| # the describe job asks Claude to write descriptions for any parameters the | |
| # mechanical fix could not source wording for | |
| # (tools/describe_router_params.py), committing them to the same PR. | |
| # | |
| # When the mechanical fix produces content identical to what is already on | |
| # the fix branch (tracked via the Sync-Content-Hash commit trailer), the | |
| # push, the Claude call, and the PR update are all skipped — so a rerun | |
| # with unchanged drift costs nothing and never clobbers previously | |
| # generated descriptions. | |
| on: | |
| workflow_dispatch: | |
| # Weekly, Wednesday 05:00 UTC — chosen to sit just ahead of a release with a | |
| # working day left to review and merge the PR. Of the last 50 stable | |
| # releases, 35 went out Thursday through Sunday and Friday was the single | |
| # most common day, so a Wednesday sweep is in front of roughly 70% of them. | |
| # (Monday, the previous setting, was ahead of more releases but staler by | |
| # several days for the ones that matter.) A release cut Monday to Wednesday | |
| # is still working from the prior week's sweep — closing that gap needs a | |
| # per-PR check, which is tracked separately. | |
| # | |
| # Paired with spec_extras_sync.yml an hour later: both make the published API | |
| # reference match the code, so they land their PRs in the same review | |
| # session, and the second run reuses the uv cache this one leaves warm. | |
| # | |
| # This ran on every push to dev touching cognee/** until it became clear how | |
| # little that bought: 594 of the last 791 commits to dev would have fired it, | |
| # roughly 20 runs a day, against drift that appears a handful of times a | |
| # month. Worse, each run force-pushed the fix branch and rewrote the PR body, | |
| # so a review in progress moved under the reviewer. The trade is that drift | |
| # merged on a Thursday now waits until the following Wednesday; dispatch | |
| # this manually if a release is going out before then. | |
| schedule: | |
| - cron: "0 5 * * 3" | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| # Queue rather than cancel: a run cancelled between the branch push and the | |
| # PR update would leave the fix PR half-refreshed. | |
| concurrency: | |
| group: router-docstring-sync | |
| cancel-in-progress: false | |
| jobs: | |
| sync-docstrings: | |
| name: Detect and fix router docstring drift | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 30 | |
| outputs: | |
| changes_made: ${{ steps.commit.outputs.changes_made }} | |
| steps: | |
| # The tree being fixed is always dev; the machinery (checker, fixer) | |
| # comes from the triggering ref, which is the default branch for both | |
| # the weekly schedule and manual dispatches. Credentials are never persisted | |
| # into the checkouts: this job imports the full cognee app (and its | |
| # dependency tree), and nothing that runs there should be able to read | |
| # a token off disk. The push/PR steps receive the PAT explicitly. | |
| - name: Check out dev | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: dev | |
| persist-credentials: false | |
| - name: Check out sync machinery from the triggering ref | |
| uses: actions/checkout@v6 | |
| with: | |
| path: automation-src | |
| persist-credentials: false | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install Python | |
| run: uv python install | |
| - name: Install dependencies | |
| run: uv sync --locked --all-extras | |
| # Imports the same FastAPI app the published OpenAPI spec is generated | |
| # from and compares each handler's documented parameters against the | |
| # parameters it actually takes. | |
| - name: Detect docstring drift | |
| id: check | |
| run: | | |
| set +e | |
| uv run python automation-src/tools/check_router_docstrings.py > docstring_report.txt | |
| code=$? | |
| set -e | |
| cat docstring_report.txt | |
| cat docstring_report.txt >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$code" -eq 2 ]; then | |
| echo "Checker failed to import the app" >&2 | |
| exit 2 | |
| fi | |
| echo "drift=$([ "$code" -eq 1 ] && echo true || echo false)" >> "$GITHUB_OUTPUT" | |
| - name: Fix docstrings | |
| if: ${{ steps.check.outputs.drift == 'true' }} | |
| run: | | |
| uv run python automation-src/tools/fix_router_docstrings.py | |
| uv run ruff format cognee | |
| # The fix must fully converge — fail loudly if anything is left. | |
| uv run python automation-src/tools/check_router_docstrings.py | |
| - name: Commit and push fix branch | |
| id: commit | |
| if: ${{ steps.check.outputs.drift == 'true' }} | |
| env: | |
| PUSH_TOKEN: ${{ secrets.REPO_DISPATCH_PAT_TOKEN }} | |
| run: | | |
| BRANCH_NAME="automation/fix-router-docstrings" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git checkout -B "${BRANCH_NAME}" | |
| git add cognee | |
| if git diff --cached --quiet; then | |
| echo "changes_made=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Skip the push — and everything downstream (Claude call, PR | |
| # update, CI on the fix PR) — when the mechanical fix produced | |
| # exactly the content already on the remote fix branch. The | |
| # cognee/ subtree hash of each sync commit is recorded as a | |
| # Sync-Content-Hash trailer; the describe job commits on top | |
| # without changing it, so its generated descriptions survive | |
| # every no-change rerun. | |
| CONTENT_HASH=$(git rev-parse "$(git write-tree):cognee") | |
| PREVIOUS_HASH="" | |
| if git fetch origin "${BRANCH_NAME}" 2>/dev/null; then | |
| PREVIOUS_HASH=$(git log FETCH_HEAD -n 5 --format=%B \ | |
| | sed -n 's/^Sync-Content-Hash: //p' | head -1) | |
| fi | |
| if [ -n "${PREVIOUS_HASH}" ] && [ "${CONTENT_HASH}" = "${PREVIOUS_HASH}" ]; then | |
| echo "Fix branch already carries this content (${CONTENT_HASH}) — skipping push." | |
| echo "changes_made=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git commit \ | |
| -m "docs: Sync router docstrings with handlers (RES-14)" \ | |
| -m "Sync-Content-Hash: ${CONTENT_HASH}" | |
| git push --force \ | |
| "https://x-access-token:${PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ | |
| "${BRANCH_NAME}" | |
| echo "changes_made=true" >> "$GITHUB_OUTPUT" | |
| echo "branch_name=${BRANCH_NAME}" >> "$GITHUB_OUTPUT" | |
| - name: Create or update fix PR | |
| if: ${{ steps.commit.outputs.changes_made == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.REPO_DISPATCH_PAT_TOKEN }} | |
| HEAD_BRANCH: ${{ steps.commit.outputs.branch_name }} | |
| run: | | |
| PR_TITLE="docs: Sync router docstrings with handlers (RES-14)" | |
| { | |
| echo "Automated router docstring sync." | |
| echo | |
| echo "The docstring checker found handlers whose documented parameters" | |
| echo "no longer match their signatures. The fixes below were generated" | |
| echo "from the handlers' own FastAPI/Pydantic metadata; a follow-up" | |
| echo "commit fills the remaining descriptions with Claude. Review the" | |
| echo "wording before merging." | |
| echo | |
| echo '```' | |
| cat docstring_report.txt | |
| echo '```' | |
| } > pr_body.md | |
| EXISTING_PR_NUMBER="$(gh pr list \ | |
| --head "${HEAD_BRANCH}" \ | |
| --base dev \ | |
| --state open \ | |
| --json number \ | |
| --jq '.[0].number // empty')" | |
| if [ -n "${EXISTING_PR_NUMBER}" ]; then | |
| # REST instead of 'gh pr edit': the edit command needs read:org | |
| # for its GraphQL query, which this repo-scoped PAT does not have. | |
| gh api "repos/${GITHUB_REPOSITORY}/pulls/${EXISTING_PR_NUMBER}" \ | |
| --method PATCH \ | |
| --field title="${PR_TITLE}" \ | |
| --field body="$(cat pr_body.md)" | |
| else | |
| gh pr create \ | |
| --base dev \ | |
| --head "${HEAD_BRANCH}" \ | |
| --title "${PR_TITLE}" \ | |
| --body-file pr_body.md | |
| fi | |
| describe-params: | |
| name: Generate missing parameter descriptions with Claude | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 15 | |
| needs: sync-docstrings | |
| # Runs whenever the sync job pushed fresh fixes; also on manual dispatch, | |
| # to fill placeholders on an already-existing fix branch. | |
| if: ${{ needs.sync-docstrings.outputs.changes_made == 'true' || github.event_name == 'workflow_dispatch' }} | |
| steps: | |
| # Dispatch runs may fire when no fix branch exists — degrade to a | |
| # no-op instead of failing at checkout. | |
| - name: Check the fix branch exists | |
| id: branch | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if gh api "repos/${GITHUB_REPOSITORY}/branches/automation/fix-router-docstrings" \ | |
| --silent 2>/dev/null; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| echo "Fix branch does not exist — nothing to describe." | |
| fi | |
| - name: Check out the fix branch | |
| if: ${{ steps.branch.outputs.exists == 'true' }} | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: automation/fix-router-docstrings | |
| persist-credentials: false | |
| - name: Check out describe machinery from the triggering ref | |
| if: ${{ steps.branch.outputs.exists == 'true' }} | |
| uses: actions/checkout@v6 | |
| with: | |
| path: automation-src | |
| persist-credentials: false | |
| - name: Install uv | |
| if: ${{ steps.branch.outputs.exists == 'true' }} | |
| uses: astral-sh/setup-uv@v7 | |
| # The script works on text and AST only — no cognee environment needed, | |
| # just the anthropic SDK in an ephemeral interpreter. Versions pinned: | |
| # the script uses beta API surface, and ruff must match the repo's | |
| # pre-commit pin so the bot never fights the formatter. | |
| - name: Generate descriptions | |
| if: ${{ steps.branch.outputs.exists == 'true' }} | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| uv run --no-project --with 'anthropic>=0.75,<1' \ | |
| python automation-src/tools/describe_router_params.py | |
| - name: Commit and push | |
| if: ${{ steps.branch.outputs.exists == 'true' }} | |
| env: | |
| PUSH_TOKEN: ${{ secrets.REPO_DISPATCH_PAT_TOKEN }} | |
| run: | | |
| uvx ruff@0.15.11 format cognee/api | |
| git add cognee | |
| if git diff --cached --quiet; then | |
| echo "No description changes to commit." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git commit -m "docs: Generate parameter descriptions with Claude (RES-14)" | |
| git push \ | |
| "https://x-access-token:${PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ | |
| HEAD:automation/fix-router-docstrings |