Repository navigation
Dispatch PR triage #23
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: Dispatch PR triage | |
| on: | |
| schedule: | |
| - cron: "23 * * * *" | |
| workflow_dispatch: | |
| permissions: | |
| actions: write | |
| pull-requests: read | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| jobs: | |
| dispatch: | |
| runs-on: ubuntu-latest | |
| env: | |
| # Runs started with workflow_dispatch skip gh-aw's daily AI credit | |
| # guardrail, so this caps how many triage runs start in any 24 hours. | |
| MAX_RUNS_PER_DAY: 20 | |
| MAX_ATTEMPTS_PER_PR: 2 | |
| steps: | |
| - name: Start triage for pull requests from external contributors | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| since=$(date -u -d '7 days ago' +%Y-%m-%d) | |
| day_ago=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) | |
| gh run list --workflow pr-triage.lock.yml --created ">=${since}" --limit 1000 \ | |
| --json displayTitle,status,conclusion,createdAt > runs.json | |
| started_today=$(jq --arg day_ago "$day_ago" '[.[] | select(.createdAt >= $day_ago)] | length' runs.json) | |
| budget=$((MAX_RUNS_PER_DAY - started_today)) | |
| # A pull request is ready for triage when it's opened, or when a draft is | |
| # marked ready for review. Both update it, so search by update time and | |
| # keep pull requests that became ready within the run history's window. | |
| # Only fork pull requests count: contributors without write access must | |
| # use forks, and members with private org membership can look external | |
| # to this token. | |
| # shellcheck disable=SC2016 | |
| gh api graphql --paginate \ | |
| -f q="repo:${GH_REPO} is:pr is:open draft:false updated:>=${since}" \ | |
| -f query=' | |
| query($q: String!, $endCursor: String) { | |
| search(query: $q, type: ISSUE, first: 100, after: $endCursor) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { | |
| ... on PullRequest { | |
| number | |
| createdAt | |
| isCrossRepository | |
| authorAssociation | |
| author { __typename } | |
| timelineItems(itemTypes: [READY_FOR_REVIEW_EVENT], last: 1) { | |
| nodes { ... on ReadyForReviewEvent { createdAt } } | |
| } | |
| } | |
| } | |
| } | |
| }' \ | |
| --jq '.data.search.nodes[] | |
| | select(.isCrossRepository) | |
| | select(.author.__typename != "Bot") | |
| | select(.authorAssociation | IN("OWNER", "MEMBER", "COLLABORATOR") | not) | |
| | {number, ready_at: ([.createdAt, .timelineItems.nodes[].createdAt] | max)}' \ | |
| > ready.json | |
| # Triage runs are named "PR Triage #<number>". Skip pull requests whose | |
| # triage since they became ready succeeded, is still queued or running, | |
| # or failed too often. | |
| jq -rs --slurpfile runs runs.json --arg since "$since" \ | |
| --argjson max_attempts "$MAX_ATTEMPTS_PER_PR" ' | |
| [$runs[0][] | {number: (.displayTitle | capture("#(?<n>[0-9]+)$").n | tonumber), status, conclusion, createdAt}] as $triages | |
| | map(select(.ready_at >= $since)) | sort_by(.ready_at)[] | |
| | . as $pr | |
| | [$triages[] | select(.number == $pr.number and .createdAt >= $pr.ready_at)] as $attempts | |
| | select(any($attempts[]; .status != "completed" or .conclusion == "success") | not) | |
| | select(($attempts | length) < $max_attempts) | |
| | .number' ready.json > candidates.txt | |
| started=0 | |
| while read -r number; do | |
| if [ "$budget" -le 0 ]; then | |
| echo "Reached the limit of ${MAX_RUNS_PER_DAY} triage runs per day." | |
| break | |
| fi | |
| gh workflow run pr-triage.lock.yml -f pr_number="$number" | |
| echo "Started triage for #${number}" | |
| started=$((started + 1)) | |
| budget=$((budget - 1)) | |
| done < candidates.txt | |
| # Let the new runs show up in "gh run list" before the next dispatch run. | |
| if [ "$started" -gt 0 ]; then | |
| sleep 15 | |
| fi |