Stale Autogenerated Docs PRs #125
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
| # Detects autogenerated docs PRs that failed to merge via auto-merge-cron. | |
| # | |
| # All four autogenerated docs workflows (agent-sdk-docs-generate, | |
| # pyairbyte-docs-generate, sync-ai-connector-docs, and | |
| # regenerate-agent-engine-api-spec) create PRs with the labels | |
| # `auto-merge` and `auto-generated`. The auto-merge-cron workflow | |
| # should merge them within ~2 hours. If a PR is still open after | |
| # 4 hours, something went wrong and a human should be notified. | |
| name: Stale Autogenerated Docs PRs | |
| permissions: | |
| contents: read | |
| on: | |
| schedule: | |
| # Every 6 hours, offset from auto-merge-cron (which runs on the hour). | |
| - cron: "30 */6 * * *" | |
| workflow_dispatch: | |
| jobs: | |
| detect-stale: | |
| name: Detect stale autogenerated docs PRs | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| # ---------- Authentication ---------- | |
| - name: Authenticate as GitHub App | |
| uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 | |
| id: get-app-token | |
| with: | |
| owner: "airbytehq" | |
| repositories: "airbyte" | |
| app-id: ${{ secrets.OCTAVIA_BOT_HOARD_APP_ID }} | |
| private-key: ${{ secrets.OCTAVIA_BOT_HOARD_PRIVATE_KEY }} | |
| # ---------- Find stale PRs ---------- | |
| - name: Find stale autogenerated docs PRs | |
| id: find-stale | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| github-token: ${{ steps.get-app-token.outputs.token }} | |
| script: | | |
| const STALE_THRESHOLD_HOURS = 4; | |
| const now = Date.now(); | |
| // Use Issues API for server-side label filtering (pulls.list | |
| // doesn't support labels, so it would paginate every open PR). | |
| const issues = await github.paginate( | |
| github.rest.issues.listForRepo, | |
| { | |
| owner: 'airbytehq', | |
| repo: 'airbyte', | |
| state: 'open', | |
| labels: 'auto-merge,auto-generated', | |
| per_page: 100, | |
| }, | |
| ); | |
| // Issues API returns both issues and PRs; keep only PRs. | |
| const prs = issues.filter((i) => i.pull_request); | |
| const stale = prs.filter((pr) => { | |
| const labels = pr.labels.map((l) => l.name); | |
| if (labels.includes('stale-notified')) { | |
| return false; | |
| } | |
| const ageMs = now - new Date(pr.created_at).getTime(); | |
| return ageMs > STALE_THRESHOLD_HOURS * 3600 * 1000; | |
| }); | |
| core.info(`Found ${stale.length} stale autogenerated docs PR(s).`); | |
| const matrix = stale.map((pr) => ({ | |
| number: pr.number, | |
| title: pr.title, | |
| created_at: pr.created_at, | |
| url: pr.html_url, | |
| })); | |
| core.setOutput('matrix', JSON.stringify(matrix)); | |
| core.setOutput('count', String(stale.length)); | |
| # ---------- Notify then label each stale PR ---------- | |
| # Label is applied *after* a successful notification dispatch so | |
| # that a failed dispatch is retried on the next cycle. | |
| - name: Notify and label stale PRs | |
| if: fromJSON(steps.find-stale.outputs.count) > 0 | |
| env: | |
| GH_TOKEN: ${{ steps.get-app-token.outputs.token }} | |
| STALE_MATRIX: ${{ steps.find-stale.outputs.matrix }} | |
| GITHUB_CI_WORKFLOW_TRIGGER_PAT: ${{ secrets.GITHUB_CI_WORKFLOW_TRIGGER_PAT }} | |
| WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| NOTIFY_TARGET: ${{ vars.STALE_DOCS_NOTIFY_TARGET || 'ian.alton@airbyte.io' }} | |
| run: | | |
| set -euo pipefail | |
| echo "$STALE_MATRIX" | jq -c '.[]' | while read -r pr_json; do | |
| pr_number=$(echo "$pr_json" | jq -r '.number') | |
| pr_title=$(echo "$pr_json" | jq -r '.title') | |
| pr_url=$(echo "$pr_json" | jq -r '.url') | |
| pr_created=$(echo "$pr_json" | jq -r '.created_at') | |
| age_seconds=$(( $(date +%s) - $(date -d "$pr_created" +%s) )) | |
| age_hours=$(( age_seconds / 3600 )) | |
| message="*Stale autogenerated docs PR detected*" | |
| message+=$'\n\n' | |
| message+="PR <${pr_url}|#${pr_number}>: ${pr_title}" | |
| message+=$'\n' | |
| message+="This pull request was created ${age_hours}h ago. I expected merge via \`auto-merge-cron\` within two hours, but the pull request is still open." | |
| message+=$'\n\n' | |
| message+="Investigate and repair this PR and, if necessary, the root cause of its failure to merge." | |
| message+=$'\n' | |
| message+="Most likely causes: CI check failure on the PR, a merge conflict with master," | |
| message+=" a path outside the \`auto-merge-cron\` allowlist (see \`validate-paths\` in \`auto-merge-cron.yml\`)," | |
| message+=" or \`auto-merge-cron\` itself failing to run (check recent runs of the \`Auto Merge\` workflow)." | |
| message+=$'\n' | |
| message+="<${WORKFLOW_RUN_URL}|View detection run>" | |
| curl -sSf -X POST \ | |
| -H "Authorization: token ${GITHUB_CI_WORKFLOW_TRIGGER_PAT}" \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| "https://api.github.com/repos/airbytehq/airbyte-ops-mcp/actions/workflows/human-in-the-loop.yml/dispatches" \ | |
| -d "$(jq -n \ | |
| --arg message "$message" \ | |
| --arg pr_url "$pr_url" \ | |
| --arg session_url "$WORKFLOW_RUN_URL" \ | |
| --arg target "$NOTIFY_TARGET" \ | |
| '{ | |
| ref: "main", | |
| inputs: { | |
| target_person: $target, | |
| message: $message, | |
| agent_session_url: $session_url, | |
| pr_url: $pr_url, | |
| header_emoji: ":warning:", | |
| header_label: "Stale Autogenerated Docs PR" | |
| } | |
| }' | |
| )" | |
| # Label only after successful dispatch so failures retry next cycle. | |
| gh pr edit "$pr_number" --repo airbytehq/airbyte --add-label "stale-notified" | |
| echo "::notice::Notified for stale PR #${pr_number} (${age_hours}h old)" | |
| done |