Migrate pull request automation away from pull_request_target #55355
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: "Pull Request Labeler" | |
| on: | |
| schedule: | |
| # Reconcile recently updated PRs promptly, including unapproved forks and | |
| # conflicted PRs for which pull_request workflows do not run. | |
| - cron: "7,22,37,52 * * * *" | |
| # Reconcile one stable shard of all open PRs each hour to recover from | |
| # delayed or missed scheduled runs. | |
| - cron: "12 * * * *" | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: "Open pull request number to reconcile" | |
| required: true | |
| type: string | |
| permissions: {} | |
| concurrency: | |
| group: pull-request-labeler | |
| cancel-in-progress: false | |
| jobs: | |
| triage: | |
| if: github.ref_name == github.event.repository.default_branch | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: .github/labeler.yml | |
| sparse-checkout-cone-mode: false | |
| - name: Collect pull requests to reconcile | |
| id: collect | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| SCHEDULE: ${{ github.event.schedule }} | |
| REQUESTED_PR: ${{ inputs.pr_number }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| if [[ ! "$REQUESTED_PR" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "Invalid pull request number: $REQUESTED_PR" | |
| exit 1 | |
| fi | |
| pr_json=$(gh api "repos/$REPO/pulls/$REQUESTED_PR") | |
| candidates=$(jq -c '[{ | |
| number: .number, | |
| head_sha: .head.sha | |
| }]' <<<"$pr_json") | |
| else | |
| pulls_json=$(gh api --paginate \ | |
| "repos/$REPO/pulls?state=open&sort=updated&direction=desc&per_page=100" | | |
| jq -cs 'add') | |
| if [ "$SCHEDULE" = "12 * * * *" ]; then | |
| shard=$(( ($(date -u +%s) / 3600) % 6 )) | |
| candidates=$(jq -c --argjson shard "$shard" \ | |
| '[.[] | select((.number % 6) == $shard) | { | |
| number: .number, | |
| head_sha: .head.sha | |
| }]' <<<"$pulls_json") | |
| else | |
| cutoff=$(date -u -d "1 hour ago" "+%Y-%m-%dT%H:%M:%SZ") | |
| # Hourly shards reconcile any candidates beyond this API budget. | |
| candidates=$(jq -c --arg cutoff "$cutoff" \ | |
| '[.[] | select(.updated_at >= $cutoff) | { | |
| number: .number, | |
| head_sha: .head.sha | |
| }][0:100]' <<<"$pulls_json") | |
| fi | |
| fi | |
| echo "Collected $(jq 'length' <<<"$candidates") pull request(s)." | |
| { | |
| echo "candidates<<EOF" | |
| echo "$candidates" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Validate pull request state | |
| id: validate | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| CANDIDATES: ${{ steps.collect.outputs.candidates }} | |
| run: | | |
| set -euo pipefail | |
| valid_numbers=() | |
| while IFS=$'\t' read -r pr_number expected_sha; do | |
| if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]] || | |
| [[ ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "Skipping malformed pull request candidate." | |
| continue | |
| fi | |
| if ! pr_json=$(gh api "repos/$REPO/pulls/$pr_number"); then | |
| echo "Pull request #$pr_number could not be fetched; skipping." | |
| continue | |
| fi | |
| if ! jq -e \ | |
| --arg repo "$REPO" \ | |
| --arg sha "$expected_sha" \ | |
| '.state == "open" and | |
| .base.repo.full_name == $repo and | |
| .head.sha == $sha and | |
| (.head.repo.full_name | type == "string")' \ | |
| >/dev/null <<<"$pr_json"; then | |
| echo "Pull request #$pr_number changed or is no longer open; skipping." | |
| continue | |
| fi | |
| valid_numbers+=("$pr_number") | |
| done < <(jq -r '.[] | [.number, .head_sha] | @tsv' <<<"$CANDIDATES") | |
| if [ "${#valid_numbers[@]}" -eq 0 ]; then | |
| echo "has_prs=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| { | |
| echo "has_prs=true" | |
| echo "pr_numbers<<EOF" | |
| printf '%s\n' "${valid_numbers[@]}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/labeler@v4 | |
| if: steps.validate.outputs.has_prs == 'true' | |
| with: | |
| repo-token: "${{ github.token }}" | |
| pr-number: ${{ steps.validate.outputs.pr_numbers }} |