Skip to content

automation | Router Docstring Sync #1

automation | Router Docstring Sync

automation | Router Docstring Sync #1

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