diff --git a/.github/workflows/check-change-note.yml b/.github/workflows/check-change-note.yml index 70b78ce72944..d29bd00486be 100644 --- a/.github/workflows/check-change-note.yml +++ b/.github/workflows/check-change-note.yml @@ -1,10 +1,11 @@ name: Check change note permissions: + contents: read pull-requests: read on: - pull_request_target: + pull_request: types: [labeled, unlabeled, opened, synchronize, reopened, ready_for_review] paths: - "*/ql/src/**/*.ql" @@ -23,7 +24,7 @@ jobs: env: REPO: ${{ github.repository }} PULL_REQUEST_NUMBER: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 512fa40d2e3a..e3f464331790 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,15 +1,144 @@ name: "Pull Request Labeler" + on: -- pull_request_target + 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: {} -permissions: - contents: read - pull-requests: write +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/labeler@v4 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + - 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<> "$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<> "$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 }}