Skip to content

Migrate pull request automation away from pull_request_target #55355

Migrate pull request automation away from pull_request_target

Migrate pull request automation away from pull_request_target #55355

Workflow file for this run

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 }}