pydantic/pydantic-ai
7 workflows · maturity 83% · 9 patterns · GitHub ↗
Practices
✓ Matrix✓ Permissions○ Security scan✓ AI review✓ Cache✓ Concurrency○ Reusable workflows
Detected patterns
Security dimensions
Workflows (7)
after-ci perms .github/workflows/after-ci.yml
View raw YAML
name: After CI
on:
workflow_run:
workflows: [CI]
types: [completed]
permissions:
statuses: write
pull-requests: write
jobs:
smokeshow:
runs-on: ubuntu-latest
steps:
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- uses: dawidd6/action-download-artifact@v6
with:
workflow: ci.yml
name: "(diff-)?coverage-html.*"
name_is_regexp: true
commit: ${{ github.event.workflow_run.head_sha }}
allow_forks: true
workflow_conclusion: completed
if_no_artifact_found: warn
- run: uvx smokeshow upload coverage-html
if: hashFiles('coverage-html/*.html') != ''
env:
SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}
SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95
SMOKESHOW_GITHUB_CONTEXT: coverage
SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }}
deploy-docs-preview:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.pull_requests[0] != null
environment:
name: deploy-docs-preview
steps:
- run: echo "$GITHUB_EVENT_JSON"
env:
GITHUB_EVENT_JSON: ${{ toJSON(github.event) }}
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- run: npm install
working-directory: docs-site
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
cache-suffix: deploy-docs-preview
- id: download-artifact
uses: dawidd6/action-download-artifact@v6
with:
workflow: ci.yml
name: site
path: site
commit: ${{ github.event.workflow_run.head_sha }}
allow_forks: true
workflow_conclusion: completed
if_no_artifact_found: warn
- uses: cloudflare/wrangler-action@v3
id: deploy
if: steps.download-artifact.outputs.found_artifact == 'true'
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: previews
workingDirectory: docs-site
command: >
deploy
--var GIT_COMMIT_SHA:${{ github.event.workflow_run.head_sha }}
--var GIT_BRANCH:${{ github.event.workflow_run.head_branch }}
- name: Set preview URL
run: uv run --no-project --with httpx .github/set_docs_pr_preview_url.py
if: steps.deploy.outcome == 'success'
env:
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
PULL_REQUEST_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
REF: ${{ github.event.workflow_run.head_sha }}
at-claude AI .github/workflows/at-claude.yml
View raw YAML
name: '@claude'
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
env:
UV_PYTHON: 3.13
UV_FROZEN: "1"
jobs:
get-pr-info:
if: |
(
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
) && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'),
github.event.comment.author_association ||
github.event.review.author_association ||
github.event.issue.author_association)
runs-on: ubuntu-latest
outputs:
is_fork: ${{ steps.pr-info.outputs.is_fork }}
pr_head_repo: ${{ steps.pr-info.outputs.pr_head_repo }}
pr_head_ref: ${{ steps.pr-info.outputs.pr_head_ref }}
steps:
- name: Get PR info
if: github.event.issue.pull_request || github.event.pull_request
id: pr-info
run: |
PR_NUMBER=${{ github.event.pull_request.number || github.event.issue.number }}
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER})
echo "pr_head_repo=$(echo "$PR_DATA" | jq -r '.head.repo.full_name')" >> $GITHUB_OUTPUT
echo "pr_head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_OUTPUT
echo "is_fork=$(echo "$PR_DATA" | jq -r '.head.repo.fork')" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
at-claude:
needs: get-pr-info
if: needs.get-pr-info.outputs.is_fork != 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: claude-code
- run: uv tool install pre-commit
- run: make install
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checks:*),Bash(gh pr list:*),Bash(gh pr create:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh run view:*),Bash(gh run list:*),Bash(git log:*),Bash(git diff:*),Bash(git grep:*),Bash(git show:*),Bash(git status:*),Bash(git add:*),Bash(git checkout:*),Bash(git commit:*),Bash(git push:*),Bash(rg:*),Bash(ls:*),Bash(tree:*),Bash(grep:*),Bash(uv run:*),Bash(make:*)"
at-claude-fork:
needs: get-pr-info
if: needs.get-pr-info.outputs.is_fork == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout fork repository
uses: actions/checkout@v6
with:
repository: ${{ needs.get-pr-info.outputs.pr_head_repo }}
ref: ${{ needs.get-pr-info.outputs.pr_head_ref }}
fetch-depth: 1
- name: Check for modified config files
run: |
PR_NUMBER=${{ github.event.pull_request.number || github.event.issue.number }}
CHANGED=$(gh pr diff "$PR_NUMBER" --name-only --repo ${{ github.repository }})
if echo "$CHANGED" | grep -qiE '(^|/)AGENTS\.md$|(^|/)CLAUDE\.md$|(^|/)\.claude/'; then
echo "::error::PR modifies agent config files (AGENTS.md, CLAUDE.md, or .claude/). Skipping for security."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checks:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh run view:*),Bash(gh run list:*),Bash(git log:*),Bash(git diff:*),Bash(git grep:*),Bash(git show:*),Bash(git status:*),Bash(rg:*),Bash(ls:*),Bash(tree:*),Bash(grep:*)"
bots AI .github/workflows/bots.yml
View raw YAML
name: PR Bots
on:
pull_request_target:
types: [opened, synchronize, labeled]
concurrency:
group: pr-bots-${{ github.event.pull_request.number }}
cancel-in-progress: ${{ github.event.action == 'synchronize' }}
jobs:
size-label:
name: Size Label
if: github.event.action != 'labeled'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Calculate PR size and set label
run: |
# Fetch file changes for this PR
FILES=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --paginate | jq -s 'add')
# Calculate lines by category (excluding uv.lock and cassettes)
CODE=$(echo "$FILES" | jq '[.[] | select(
(.filename | (startswith("tests/") or startswith("docs/") or endswith(".md")) | not) and
(.filename != "uv.lock") and
(.filename | contains("/cassettes/") | not)
) | .additions + .deletions] | add // 0')
DOCS=$(echo "$FILES" | jq '[.[] | select(
(.filename | (startswith("docs/") or endswith(".md"))) and
(.filename != "uv.lock") and
(.filename | contains("/cassettes/") | not)
) | .additions + .deletions] | add // 0')
TESTS=$(echo "$FILES" | jq '[.[] | select(
(.filename | startswith("tests/")) and
(.filename | contains("/cassettes/") | not) and
(.filename | endswith(".md") | not)
) | .additions + .deletions] | add // 0')
# Calculate weighted score: code + 50% docs + 50% tests
SCORE=$((CODE + DOCS / 2 + TESTS / 2))
echo "Code: $CODE, Docs: $DOCS, Tests: $TESTS"
echo "Weighted score: $SCORE"
# Determine size label based on cutoffs
if [ $SCORE -le 100 ]; then
SIZE="size: S"
elif [ $SCORE -le 500 ]; then
SIZE="size: M"
elif [ $SCORE -le 1500 ]; then
SIZE="size: L"
else
SIZE="size: XL"
fi
echo "Size: $SIZE"
# Remove any existing size labels (except the one we're setting) via API
for label in "size: S" "size: M" "size: L" "size: XL"; do
if [ "$label" != "$SIZE" ]; then
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/${label}" --method DELETE 2>/dev/null || true
fi
done
# Add the new size label via API
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" --method POST -f "labels[]=$SIZE"
echo "Set label: $SIZE (score: $SCORE)"
env:
GH_TOKEN: ${{ github.token }}
# Security: The classify job runs the LLM with READ-ONLY permissions and no label API access.
# The LLM's output is validated against an allowlist before the apply job takes any write action.
# This prevents prompt injection from adding arbitrary labels (e.g. 'auto-review' to trigger the review job).
category-classify:
name: Category Classify
if: github.event.action != 'labeled'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
category: ${{ steps.extract.outputs.category }}
skip: ${{ steps.check-label.outputs.has_label }}
steps:
- name: Check for modified config files
if: github.event.pull_request.head.repo.fork
run: |
CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only --repo ${{ github.repository }})
if echo "$CHANGED" | grep -qiE '(^|/)AGENTS\.md$|(^|/)CLAUDE\.md$|(^|/)\.claude/'; then
echo "::error::PR modifies agent config files (AGENTS.md, CLAUDE.md, or .claude/). Skipping auto-labeling for security."
exit 1
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Check if category label already exists
id: check-label
run: |
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json labels --jq '.labels[].name')
CATEGORY_LABELS=("bug" "feature" "docs" "chore" "dependency")
for label in "${CATEGORY_LABELS[@]}"; do
if echo "$LABELS" | grep -q "^${label}$"; then
echo "has_label=true" >> $GITHUB_OUTPUT
echo "PR already has category label: $label"
exit 0
fi
done
echo "has_label=false" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout repository
if: steps.check-label.outputs.has_label == 'false'
uses: actions/checkout@v6
- name: Classify PR with Claude Code
if: steps.check-label.outputs.has_label == 'false'
id: classify
uses: anthropics/claude-code-action@v1
env:
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_CODE_BASE_URL }}
with:
anthropic_api_key: ${{ secrets.CLAUDE_CODE_API_KEY || secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
allowed_non_write_users: "*"
claude_args: |
--allowedTools "Bash(gh pr view:*),Bash(gh pr diff:*)"
--json-schema '{"type":"object","properties":{"category":{"type":"string"}},"required":["category"]}'
prompt: |
Classify PR #${{ github.event.pull_request.number }} in ${{ github.repository }}.
Run `gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }}` for the title and description.
Run `gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only` for the changed files.
Categories:
- bug: Fixes broken behavior
- feature: New functionality
- docs: Documentation-only (no code changes)
- chore: CI, refactoring, dev dependencies, tests-only
- dependency: Production dependency updates
When both code and docs change, prefer `feature` or `bug` over `docs`.
If pyproject.toml files changed, run `gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }}` to see the full diff and distinguish production deps (`dependency`) from dev-only deps in `[dependency-groups]` (`chore`).
Return your classification as JSON with a "category" field set to one of: bug, feature, docs, chore, dependency.
- name: Extract and validate category
if: steps.check-label.outputs.has_label == 'false'
id: extract
run: |
CATEGORY=$(echo "$STRUCTURED_OUTPUT" | jq -r .category | tr -d '[:space:]')
ALLOWED="bug feature docs chore dependency"
if ! echo "$ALLOWED" | grep -Fqw "$CATEGORY"; then
echo "::error::Invalid category '$CATEGORY' from Claude — must be one of: $ALLOWED"
exit 1
fi
echo "category=$CATEGORY" >> $GITHUB_OUTPUT
echo "Classified as: $CATEGORY"
env:
STRUCTURED_OUTPUT: ${{ steps.classify.outputs.structured_output }}
category-apply:
name: Category Apply
needs: category-classify
if: needs.category-classify.outputs.skip != 'true' && needs.category-classify.outputs.category != ''
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Validate and apply category label
run: |
CATEGORY="${{ needs.category-classify.outputs.category }}"
ALLOWED="bug feature docs chore dependency"
if ! echo "$ALLOWED" | grep -Fqw "$CATEGORY"; then
echo "::error::Invalid category '$CATEGORY' — must be one of: $ALLOWED"
exit 1
fi
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \
--method POST -f "labels[]=$CATEGORY"
echo "Applied label: $CATEGORY"
env:
GH_TOKEN: ${{ github.token }}
review:
name: Review
needs: [size-label, category-apply]
if: >-
!failure() && !cancelled() &&
github.event.action == 'labeled' && github.event.label.name == 'auto-review'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
issues: write
pull-requests: write
actions: read
steps:
- uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Check for modified config files
if: github.event.pull_request.head.repo.fork
run: |
CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only --repo ${{ github.repository }})
if echo "$CHANGED" | grep -qiE '(^|/)AGENTS\.md$|(^|/)CLAUDE\.md$|(^|/)\.claude/'; then
echo "::error::PR modifies agent config files (AGENTS.md, CLAUDE.md, or .claude/). Skipping auto-review for security."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Select review model
id: model
run: |
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json labels --jq '.labels[].name')
echo "Labels: $LABELS"
CATEGORY=$(echo "$LABELS" | grep -E '^(bug|feature|docs|chore|dependency)$' | head -1)
SIZE=$(echo "$LABELS" | grep -E '^size: ' | head -1)
# Default to Opus for large/complex PRs
MODEL="claude-opus-4-6[1m]"
# Disabled for now because Sonnet reviews have been disappointing
# # Use Sonnet for docs, dependency, chore, or small/medium PRs
# if [ "$CATEGORY" = "docs" ] || [ "$CATEGORY" = "dependency" ] || [ "$CATEGORY" = "chore" ] || [ "$SIZE" = "size: S" ] || [ "$SIZE" = "size: M" ]; then
# MODEL="claude-sonnet-4-5[1m]"
# fi
echo "model=$MODEL" >> $GITHUB_OUTPUT
echo "Selected model: $MODEL (category: $CATEGORY, size: $SIZE)"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Gather PR context
run: |
# Use the script from the base repo (main branch), not the fork
gh api "repos/${REPO}/contents/scripts/gather-review-context.sh?ref=${BASE_REF}" --jq .content | base64 -d > /tmp/gather-review-context.sh
bash /tmp/gather-review-context.sh "$PR_NUMBER" "$REPO"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- uses: anthropics/claude-code-action@v1
env:
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_CODE_BASE_URL }}
with:
anthropic_api_key: ${{ secrets.CLAUDE_CODE_API_KEY || secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
display_report: 'true'
additional_permissions: |
actions: read
claude_args: |
--model ${{ steps.model.outputs.model }}
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checks:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh run view:*),Bash(gh run list:*),Bash(gh api repos/${{ github.repository }}/pulls/comments/:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews:*),Bash(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments:*),Bash(git log:*),Bash(git diff:*),Bash(git grep:*),Bash(git show:*),Bash(git status:*),Bash(jq:*),Bash(cat:*),Bash(rg:*),Bash(ls:*),Bash(tree:*),Bash(grep:*),WebSearch,WebFetch"
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
PR AUTHOR: ${{ github.event.pull_request.user.login }} (${{ github.event.pull_request.author_association }})
Review this pull request. The PR branch is already checked out in the current working directory and the `CLAUDE.md` at the root (symlinked to `AGENTS.md`) is already loaded in your system prompt.
# What to look for
- If the PR should not have been created yet (e.g. no issue, insufficiently defined scope, not ready for implementation, duplicate of existing open PR), just leave a comment informing the user and maintainer of this and don't bother doing a thorough review.
- Any change that does not align with the project's standards, philosophy, or requirements for every contribution as stated in `AGENTS.md` (symlinked from `CLAUDE.md`).
- Any change that does not match maintainer guidance in the issue or earlier PR comments on what an acceptable solution would look like.
- Any change or design decision or tradeoff (in both behavior and API) that needs explicit consideration, discussion, or maintainer awareness and approval.
- Any line of code that violates the concrete guidelines/rules laid out in the relevant `AGENTS.md` file(s): the top-level guidelines apply to all changes, while directory-specific guidelines affect only the changes in that directory.
- Anything else that the responsibilities you are assigned in `AGENTS.md` suggest that you should be calling out: use your best judgment.
Generally, the priority in terms of "crucial to get right" and "what to focus on first in a new PR" is public API > concepts and behavior > documentation > tests > code style.
If the PR has high level problems that will likely require significant changes at lower levels, hold off on looking for or commenting on lower level problems until the higher level problems are addressed, so that the PR author (and your context window) don't get overwhelmed.
Note that while another agent (Devin) is responsible for thoroughly reviewing the implementation for bugs, security issues, and edge cases,
_you_ are responsible for catching every violation of the repository's standards and guidelines listed in the `AGENTS.md` and `agent_docs/*.md` files.
Do not focus exclusively on high-level concerns: by the time the author has addressed every comment you've left over multiple rounds of review, the PR should be ready to merge.
# Gathering context
Before doing anything else, read ALL of the following pre-gathered context files in a single parallel tool call:
- `.github/.review-context/pr-details.json` — PR title, body, author, branch info, labels, state, review decision, timestamps
- `.github/.review-context/pr-comments.txt` — existing top-level PR comments
- `.github/.review-context/review-comments.txt` — existing inline review comments (resolved+outdated threads and threads predating the last auto-review are collapsed to one-liners with comment IDs so you can fetch full details if needed)
- `.github/.review-context/related-issues.txt` — linked issues and their comments
- `.github/.review-context/changed-files.txt` — changed files with per-file addition/deletion counts (tab-separated; non-generated files include a third column with the path to their per-file diff)
- `.github/.review-context/agents-md.txt` — directory-specific `AGENTS.md` files for changed directories
- `agent_docs/index.md` - repo-wide coding guidelines
The diff is split into per-file diffs under `.github/.review-context/diff/` (excluding `uv.lock` and cassettes), that you can read on demand and in parallel.
The diffs include function-level context (`git diff -W`), so you can see the full function/method being modified without needing to read the source file separately.
Each commentable line in the diff is prefixed with its source line number: `NL:<number>` for new or context lines, `OL:<number>` for deleted lines.
For newly added files, the diff contains the complete file contents — do not re-read these from disk.
The diff file paths are listed in the third column of `changed-files.txt`.
The pre-gathered diffs are the source of truth for what this PR changes. Do not re-fetch diffs or file lists using `gh pr diff` or `gh api`.
When you need code context beyond what the diffs provide, use the `Read` tool on the checked-out source files.
Use the `gh` CLI only when you need additional information not already in these files (e.g. to read other referenced PRs or issues, check CI status, or read files excluded from the gathered diff).
Use specific `gh` subcommands (`gh pr view`, `gh issue view`, `gh run view`, etc.) rather than `gh api` for most queries.
`gh api` is scoped to comment and review endpoints on this PR only:
- `gh api repos/${{ github.repository }}/pulls/comments/<id>` — individual review comment by ID
- `gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments` — list review comments
- `gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews` — list reviews
- `gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments` — list issue comments
Be careful about loading large diffs if you're unlikely to need them yet, like massive test files when there's plenty of more interesting code to comment on first, as you don't want to blow your context window too early.
You will usually want to read all the "core implementation" and docs diffs in one go, though, so you have the full context of the PR as you identify problems, instead of going file by file.
# Posting comments
While gathering context and learning about the PR, keep track of problems/points of discussion as you find them, and wait to post comments until the end,
as the comments you write will be better, less duplicative and more focused on the changes that really matter if you have the full set of problems as context.
For each identified issue that is determined to be worth a new comment, use `mcp__github_inline_comment__create_inline_comment` to attach the feedback to a specific line of code.
- Only lines with an `NL:` or `OL:` prefix in the diff are commentable. For OL lines, use `side: LEFT`.
- Include the reasoning, but don't quote specific rules from the `AGENTS.md` files.
- Include a concrete suggestion if appropriate (but to not use ` ```suggestion ` blocks as they can render incorrectly when the line numbers are off)
- Include a ping to the maintainer (`@DouweM`) on any change that requires maintainer input before the PR author can move forward.
- If the same issue shows up in multiple places, post a comment on each instance but have later comments refer to the first comment using a link.
- Use `gh pr comment` only for important feedback that doesn't relate to a specific line or file, not for a summary of feedback you've already posted inline.
Your comments should be:
- actionable: they should request a change, flag a concern that needs discussion, and/or suggest an improvement; don't comment on positive aspects of the PR like "excellent design choices".
- concise and to the point: don't use unnecessary emojis, lists, or subheadings, but do link to code if appropriate; 1 to 3 paragraphs are pretty much always enough.
- friendly without being sycophantic: use the tone and language of a helpful and encouraging project maintainer, but no need to compliment the author on positive aspects of the PR or point out changes that are good.
- non-repetitive: don't repeat things pointed out in earlier review comments, unless it looks like they'll be forgotten if you don't point them out; e.g. when they're marked as resolved/outdated but the problem persists without a satisfactory resolution (like a maintainer comment saying the comment does not need to be addressed).
You are meant to be helpful to the contributor and the maintainer, so your comments should never add noise to the conversation:
- Do not post a final summary comment; inline comments are sufficient.
- Do not comment on lines that do not need improvement, maintainer awareness, or discussion; comments pointing out a good choice are just noise.
- Do not post multiple comments for the same exact issue unless it shows up in different places.
It bears repeating that you are the first line of defense against low-quality contributions and maintainer headaches, and you have a big role in ensuring that every contribution to this project meets or exceeds the high standards that the Pydantic brand is known and loved for.
- name: Remove auto-review label
if: always()
run: gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/auto-review" --method DELETE 2>/dev/null || true
env:
GH_TOKEN: ${{ github.token }}
ci matrix perms .github/workflows/ci.yml
View raw YAML
name: CI
on:
push:
branches:
- main
tags:
- "**"
pull_request: {}
env:
COLUMNS: 150
UV_PYTHON: 3.12
UV_FROZEN: "1"
permissions:
contents: read
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.13"
enable-cache: true
cache-suffix: lint
- name: Install dependencies
run: uv sync --all-extras --no-extra outlines-vllm-offline --all-packages --group lint
- uses: pre-commit/action@v3.0.0
with:
extra_args: --all-files --verbose
env:
SKIP: no-commit-to-branch
- run: uv build --all-packages
- run: ls -lh dist/
# mypy and lint are a bit slower than other jobs, so we run them separately
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: mypy
- name: Install dependencies
run: uv sync --no-dev --group lint
- run: make typecheck-mypy
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: docs
- run: uv sync --group docs
- run: make docs
- run: tree -sh site
- uses: actions/setup-node@v4
- run: npm install
working-directory: docs-site
- run: npm run typecheck
working-directory: docs-site
- name: Store docs
uses: actions/upload-artifact@v4
with:
name: site
path: site
# check all docs images are tinified, You'll need an API key from https://tinify.com/ to fix this if it fails
- run: uvx tinicly docs --check
test:
name: test on ${{ matrix.python-version }} (${{ matrix.install.name }})
# Use Depot 4-core runners for all-extras (the slowest variant) when run by maintainers or opted in via 'ci:fast' label
# Use 'ci:slow' label to force standard GitHub runners (e.g. during Depot outages)
runs-on: >-
${{
!contains(github.event.pull_request.labels.*.name, 'ci:slow')
&& matrix.install.name == 'all-extras'
&& (
github.event.pull_request.head.repo.full_name == github.repository
|| contains(github.event.pull_request.labels.*.name, 'ci:fast')
)
&& 'depot-ubuntu-24.04-4'
|| 'ubuntu-latest'
}}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
install:
- name: pydantic-ai-slim
command: "--package pydantic-ai-slim"
- name: standard
command: ""
- name: all-extras
command: "--all-extras --no-extra outlines-vllm-offline"
env:
CI: true
COVERAGE_PROCESS_START: ./pyproject.toml
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: ${{ matrix.install.name }}
- run: mkdir .coverage
- run: uv sync --only-dev
- name: cache HuggingFace models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
hf-${{ runner.os }}-
- run: uv run ${{ matrix.install.command }} coverage run -m pytest --durations=100 -n auto --dist=loadgroup
env:
COVERAGE_FILE: .coverage/.coverage.${{ matrix.python-version }}-${{ matrix.install.name }}
- name: store coverage files
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.install.name }}
path: .coverage
include-hidden-files: true
test-lowest-versions:
name: test on ${{ matrix.python-version }} (lowest-versions)
# Use Depot 4-core runners for maintainers or opted in via 'ci:fast' label
# Use 'ci:slow' label to force standard GitHub runners (e.g. during Depot outages)
runs-on: >-
${{
!contains(github.event.pull_request.labels.*.name, 'ci:slow')
&& (
github.event.pull_request.head.repo.full_name == github.repository
|| contains(github.event.pull_request.labels.*.name, 'ci:fast')
)
&& 'depot-ubuntu-24.04-4'
|| 'ubuntu-latest'
}}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
env:
CI: true
COVERAGE_PROCESS_START: ./pyproject.toml
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: lowest-versions
- run: mkdir .coverage
- run: uv sync --group dev
- name: cache HuggingFace models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
hf-${{ runner.os }}-
- run: unset UV_FROZEN
- run: uv run --all-extras --no-extra outlines-vllm-offline --resolution lowest-direct coverage run -m pytest --durations=100 -n auto --dist=loadgroup
env:
COVERAGE_FILE: .coverage/.coverage.${{matrix.python-version}}-lowest-versions
- name: store coverage files
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python-version }}-lowest-versions
path: .coverage
include-hidden-files: true
test-examples:
name: test examples on ${{ matrix.python-version }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
env:
CI: true
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: examples
- name: cache HuggingFace models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
hf-${{ runner.os }}-
- run: uv run --all-extras --no-extra outlines-vllm-offline python tests/import_examples.py
coverage:
runs-on: ubuntu-latest
needs: [test, test-lowest-versions]
steps:
- uses: actions/checkout@v6
with:
# needed for diff-cover
fetch-depth: 0
- name: get coverage files
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: .coverage
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: dev
- run: uv sync --group dev
- run: uv run coverage combine
- run: uv run coverage report
- run: uv run strict-no-cover
env:
COVERAGE_FILE: .coverage/.coverage
- run: uv run coverage html --show-contexts --title "Pydantic AI coverage for ${{ github.sha }}"
- uses: actions/upload-artifact@v4
with:
name: coverage-html
path: htmlcov
include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
check:
if: always()
needs:
- lint
- mypy
- docs
- test
- test-lowest-versions
- test-examples
- coverage
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
deploy-docs:
needs: [check]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
environment:
name: deploy-docs
url: https://ai.pydantic.dev
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- run: npm install
working-directory: docs-site
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: docs-upload
- uses: actions/download-artifact@v4
with:
name: site
path: site
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
workingDirectory: docs-site
command: >
deploy
--var GIT_COMMIT_SHA:${{ github.sha }}
--var GIT_BRANCH:main
- run: uv sync --group docs-upload
- run: uv run python docs/.hooks/algolia.py upload
env:
ALGOLIA_WRITE_API_KEY: ${{ secrets.ALGOLIA_WRITE_API_KEY }}
deploy-docs-preview:
needs: [check]
if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: deploy-docs-preview
permissions:
deployments: write
statuses: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- run: npm install
working-directory: docs-site
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: deploy-docs-preview
- uses: actions/download-artifact@v4
with:
name: site
path: site
- uses: cloudflare/wrangler-action@v3
id: deploy
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: previews
workingDirectory: docs-site
command: >
deploy
--var GIT_COMMIT_SHA:${{ github.sha }}
--var GIT_BRANCH:main
- name: Set preview URL
run: uv run --no-project --with httpx .github/set_docs_main_preview_url.py
env:
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
REF: ${{ github.sha }}
# TODO(Marcelo): We need to split this into two jobs: `build` and `release`.
release:
needs: [check]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
environment:
name: release
url: https://pypi.org/project/pydantic-ai/${{ steps.inspect_package.outputs.version }}
permissions:
id-token: write
outputs:
package-version: ${{ steps.inspect_package.outputs.version }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: release
- run: uv build --all-packages
- name: Inspect package version
id: inspect_package
run: |
uv tool install --with uv-dynamic-versioning hatchling
version=$(uvx hatchling version)
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
send-tweet:
name: Send tweet
needs: [release]
if: needs.release.result == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install tweepy==4.14.0
- name: Send tweet
shell: python
run: |
import os
import tweepy
client = tweepy.Client(
access_token=os.getenv("TWITTER_ACCESS_TOKEN"),
access_token_secret=os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
consumer_key=os.getenv("TWITTER_CONSUMER_KEY"),
consumer_secret=os.getenv("TWITTER_CONSUMER_SECRET"),
)
version = os.getenv("VERSION").strip('"')
tweet = os.getenv("TWEET").format(version=version)
client.create_tweet(text=tweet)
env:
VERSION: ${{ needs.release.outputs.package-version }}
TWEET: |
Pydantic AI version {version} is out! 🎉
https://github.com/pydantic/pydantic-ai/releases/tag/v{version}
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
manually-deploy-docs .github/workflows/manually-deploy-docs.yml
View raw YAML
name: Manual Docs Deploy
on:
workflow_dispatch:
jobs:
build-docs-manual:
# Note: this should match the `docs` job in ci.yml
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: docs
- run: uv sync --group docs
- run: make docs
- run: tree -sh site
- uses: actions/setup-node@v4
- run: npm install
working-directory: docs-site
- run: npm run typecheck
working-directory: docs-site
- name: Store docs
uses: actions/upload-artifact@v4
with:
name: site
path: site
deploy-docs-manual:
# Note: this should match the `deploy-docs` job in ci.yml
needs: [build-docs-manual]
runs-on: ubuntu-latest
environment:
name: deploy-docs
url: https://ai.pydantic.dev
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- run: npm install
working-directory: docs-site
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-suffix: docs-upload
- uses: actions/download-artifact@v4
with:
name: site
path: site
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
workingDirectory: docs-site
command: >
deploy
--var GIT_COMMIT_SHA:${{ github.sha }}
--var GIT_BRANCH:main
- run: uv sync --group docs-upload
- run: uv run python docs/.hooks/algolia.py upload
env:
ALGOLIA_WRITE_API_KEY: ${{ secrets.ALGOLIA_WRITE_API_KEY }}
pr-guard perms .github/workflows/pr-guard.yml
View raw YAML
name: PR Guard
on:
pull_request_target:
types: [opened, ready_for_review, edited]
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: pr-guard-${{ github.event.pull_request.number }}
jobs:
guard:
name: Guard
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check linked issues and guard against duplicates
run: |
set -euo pipefail
PR_NUMBER=${{ github.event.pull_request.number }}
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
AUTHOR_ASSOCIATION="${{ github.event.pull_request.author_association }}"
PR_BODY=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body // ""')
echo "PR #${PR_NUMBER} by ${PR_AUTHOR} (${AUTHOR_ASSOCIATION})"
# Skip draft PRs — authors may create drafts to save progress
if [ "${{ github.event.pull_request.draft }}" = "true" ]; then
echo "PR is a draft, skipping checks."
exit 0
fi
# Maintainer bypass
if [[ "$AUTHOR_ASSOCIATION" == "MEMBER" || "$AUTHOR_ASSOCIATION" == "OWNER" || "$AUTHOR_ASSOCIATION" == "COLLABORATOR" ]]; then
echo "Author is a maintainer/collaborator, skipping checks."
exit 0
fi
# Parse closing keywords from PR body
# GitHub recognizes: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
# Case-insensitive, with optional leading - or *
ISSUE_NUMBERS=$(printf '%s\n' "$PR_BODY" | grep -ioE '(^|\s|[-*]\s*)(close[sd]?|fix(es|ed)?|resolve[sd]?)\s+#[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
if [ -z "$ISSUE_NUMBERS" ]; then
echo "No closing keywords found in PR body."
exit 0
fi
echo "Found linked issues: $ISSUE_NUMBERS"
# Helper: attempt to assign a user to an issue.
# GitHub silently ignores assignees who lack push access (returns 201 but doesn't add them).
# We check assignability first via GET /repos/{owner}/{repo}/assignees/{user} (204 = yes, 404 = no).
try_assign() {
local issue_num="$1"
local user="$2"
if gh api "repos/${REPO}/assignees/${user}" > /dev/null 2>&1; then
gh api "repos/${REPO}/issues/${issue_num}/assignees" --method POST -f "assignees[]=${user}" > /dev/null
echo "Assigned ${user} to issue #${issue_num}."
return 0
else
echo "Cannot assign ${user} to issue #${issue_num} (user is not assignable to this repo)."
return 1
fi
}
# Fetch all open PRs once upfront (the list endpoint includes body and labels).
# Done outside the loop to avoid repeated paginated calls when multiple issues are referenced.
ALL_OPEN_PRS=$(gh api "repos/${REPO}/pulls?state=open&per_page=100" --paginate) || ALL_OPEN_PRS="[]"
FOUND_OPEN_ISSUE="false"
FOUND_CLOSED_ISSUE="false"
for ISSUE_NUM in $ISSUE_NUMBERS; do
echo ""
echo "--- Checking issue #${ISSUE_NUM} ---"
# Fetch issue details, skip if not found (gh api exits non-zero on 404)
if ! ISSUE_JSON=$(gh api "repos/${REPO}/issues/${ISSUE_NUM}" 2>/dev/null); then
echo "Issue #${ISSUE_NUM} not found, skipping."
continue
fi
# Skip if this is actually a pull request
IS_PR=$(echo "$ISSUE_JSON" | jq 'has("pull_request")')
if [ "$IS_PR" = "true" ]; then
echo "#${ISSUE_NUM} is a pull request, not an issue. Skipping."
continue
fi
# Skip closed issues — duplicate check only applies to open issues
ISSUE_STATE=$(echo "$ISSUE_JSON" | jq -r '.state')
if [ "$ISSUE_STATE" != "open" ]; then
echo "Issue #${ISSUE_NUM} is ${ISSUE_STATE}, skipping."
FOUND_CLOSED_ISSUE="true"
continue
fi
FOUND_OPEN_ISSUE="true"
# Check for duplicate/blocking PRs first — this must run regardless of assignment outcome.
# Look for any other open PR (excluding this one) that references this issue.
echo "Checking for existing PRs targeting issue #${ISSUE_NUM}..."
BLOCKING_PRS=""
FOUND_STALE_PR="false"
MATCHING_PRS=$(echo "$ALL_OPEN_PRS" | jq -c "[.[] | select(.number != ${PR_NUMBER} and (.body // \"\" | test(\"(?i)\\\\b(close[sd]?|fix(es|ed)?|resolve[sd]?)\\\\s+#${ISSUE_NUM}(\\\\s|$|[^0-9])\")))] | .[]")
while IFS= read -r PR_JSON; do
[ -z "$PR_JSON" ] && continue
EXISTING_PR_NUM=$(echo "$PR_JSON" | jq -r '.number')
EXISTING_PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.user.login')
echo "Found PR #${EXISTING_PR_NUM} by ${EXISTING_PR_AUTHOR} that references issue #${ISSUE_NUM}."
# Check if the existing PR has the Stale label
HAS_STALE=$(echo "$PR_JSON" | jq '[.labels[].name] | any(. == "Stale")')
if [ "$HAS_STALE" = "true" ]; then
echo "PR #${EXISTING_PR_NUM} is stale. Allowing new PR to supersede."
FOUND_STALE_PR="true"
continue
fi
# Collect all non-stale blocking PRs
if [ -z "$BLOCKING_PRS" ]; then
BLOCKING_PRS="#${EXISTING_PR_NUM}"
else
BLOCKING_PRS="${BLOCKING_PRS}, #${EXISTING_PR_NUM}"
fi
done <<< "$MATCHING_PRS"
if [ -n "$BLOCKING_PRS" ]; then
echo "Closing PR #${PR_NUMBER} — issue #${ISSUE_NUM} already has active PRs: ${BLOCKING_PRS}"
COMMENT=$(printf '%s\n\n%s\n\n%s' \
"Thanks for your interest in this issue! However, there are already open PRs addressing issue #${ISSUE_NUM}: ${BLOCKING_PRS}." \
"To avoid duplicate efforts, this PR has been closed. If you'd like to contribute, you can review the existing PRs or share your thoughts on [issue #${ISSUE_NUM}](https://github.com/${REPO}/issues/${ISSUE_NUM})." \
"If you believe the existing PRs are inactive, please comment on the issue and a maintainer can reassess.")
gh pr close "$PR_NUMBER" --repo "$REPO"
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$COMMENT" || true
exit 0
fi
if [ "$FOUND_STALE_PR" = "true" ]; then
echo "All existing PRs for issue #${ISSUE_NUM} are stale. Allowing new PR."
fi
# Now handle assignment (best-effort, does not gate duplicate check above)
ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '[.assignees[].login] | join(",")')
echo "Current assignees: ${ASSIGNEES:-none}"
if [ -z "$ASSIGNEES" ]; then
# No assignee — attempt to assign PR author (may fail if user lacks push access)
try_assign "$ISSUE_NUM" "$PR_AUTHOR" || true
elif echo ",$ASSIGNEES," | grep -q ",${PR_AUTHOR},"; then
echo "Issue #${ISSUE_NUM} is already assigned to ${PR_AUTHOR}."
else
echo "Issue #${ISSUE_NUM} is assigned to someone else. Leaving assignment as-is for maintainers to review."
fi
done
# If we found closed issues but no open ones, close the PR
if [ "$FOUND_CLOSED_ISSUE" = "true" ] && [ "$FOUND_OPEN_ISSUE" = "false" ]; then
echo "All referenced issues are closed. Closing PR."
gh pr close "$PR_NUMBER" --repo "$REPO"
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "All issues referenced by this PR are already closed. If you believe an issue should be reopened, please comment on it first." || true
fi
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
stale perms .github/workflows/stale.yml
View raw YAML
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 14 * * *'
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
any-of-issue-labels: 'question,more info'
days-before-issue-stale: 7
days-before-issue-close: 3
stale-issue-message: 'This issue is stale, and will be closed in 3 days if no reply is received.'
close-issue-message: 'Closing this issue as it has been inactive for 10 days.'
any-of-pr-labels: 'awaiting author revision'
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: 'This PR is stale, and will be closed in 7 days if no reply is received.'
close-pr-message: 'Closing this PR as it has been inactive for 21 days.'