assign-reviewers.yml

  1# Assign Reviewers — Smart team assignment based on diff weight
  2#
  3# Triggers on PR open and ready_for_review events. Checks out the coordinator
  4# repo (zed-industries/codeowner-coordinator) to access the assignment script and rules,
  5# then assigns the 1-2 most relevant teams as reviewers.
  6#
  7# NOTE: This file is stored in the codeowner-coordinator repo but must be deployed to
  8# the zed repo at .github/workflows/assign-reviewers.yml. See INSTALL.md.
  9#
 10# AUTH NOTE: Uses a GitHub App (COORDINATOR_APP_ID + COORDINATOR_APP_PRIVATE_KEY)
 11# for all API operations: cloning the private coordinator repo, requesting team
 12# reviewers, and setting PR assignees. GITHUB_TOKEN is not used.
 13#
 14# SECURITY INVARIANTS (pull_request_target):
 15# This workflow runs with access to secrets for ALL PRs including forks.
 16# It is safe ONLY because:
 17#   1. The checkout is the coordinator repo at ref: main — NEVER the PR head/branch
 18#   2. No ${{ }} interpolation of event fields in run: blocks — all routed via env:
 19#   3. The script never executes, sources, or reads files from the PR branch
 20# Violating any of these enables remote code execution with secret access.
 21
 22name: Assign Reviewers
 23
 24on:
 25  # zizmor: ignore[dangerous-triggers] reviewed — no PR code checkout, only coordinator repo at ref: main
 26  pull_request_target:
 27    types: [opened, ready_for_review]
 28
 29# GITHUB_TOKEN is not used — all operations use the GitHub App token.
 30# Declare minimal permissions so the default token has no write access.
 31permissions: {}
 32
 33# Prevent duplicate runs for the same PR (e.g., rapid push + ready_for_review).
 34concurrency:
 35  group: assign-reviewers-${{ github.event.pull_request.number }}
 36  cancel-in-progress: true
 37
 38# NOTE: For ready_for_review events, the webhook payload may still carry
 39# draft: true due to a GitHub race condition (payload serialized before DB
 40# update). We trust the event type instead — the script rechecks draft status
 41# via a live API call as defense-in-depth.
 42#
 43# No author_association filter — external and fork PRs also get reviewer
 44# assignments. Assigned reviewers are inherently scoped to org team members
 45# by the GitHub Teams API.
 46jobs:
 47  assign-reviewers:
 48    if: >-
 49      github.event.action == 'ready_for_review' || github.event.pull_request.draft == false
 50    runs-on: ubuntu-latest
 51    steps:
 52      - name: Generate app token
 53        id: app-token
 54        uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
 55        with:
 56          app-id: ${{ vars.COORDINATOR_APP_ID }}
 57          private-key: ${{ secrets.COORDINATOR_APP_PRIVATE_KEY }}
 58          repositories: codeowner-coordinator,zed
 59
 60      # SECURITY: checks out the coordinator repo at ref: main, NOT the PR branch.
 61      # persist-credentials: false prevents the token from leaking into .git/config.
 62      - name: Checkout coordinator repo
 63        uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
 64        with:
 65          repository: zed-industries/codeowner-coordinator
 66          ref: main
 67          path: codeowner-coordinator
 68          token: ${{ steps.app-token.outputs.token }}
 69          persist-credentials: false
 70
 71      - name: Setup Python
 72        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
 73        with:
 74          python-version: "3.11"
 75
 76      - name: Install dependencies
 77        run: |
 78          pip install --no-deps -q --only-binary ':all:' \
 79            -r /dev/stdin <<< "pyyaml==6.0.3 --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"
 80
 81      - name: Assign reviewers
 82        env:
 83          GH_TOKEN: ${{ steps.app-token.outputs.token }}
 84          PR_URL: ${{ github.event.pull_request.html_url }}
 85          TARGET_REPO: ${{ github.repository }}
 86          ASSIGN_INTERNAL: ${{ vars.ASSIGN_INTERNAL || 'false' }}
 87          ASSIGN_EXTERNAL: ${{ vars.ASSIGN_EXTERNAL || 'true' }}
 88        run: |
 89          cd codeowner-coordinator
 90          python .github/scripts/assign-reviewers.py \
 91            --pr "$PR_URL" \
 92            --apply \
 93            --rules-file team-membership-rules.yml \
 94            --repo "$TARGET_REPO" \
 95            --org zed-industries \
 96            2>&1 | tee /tmp/assign-reviewers-output.txt
 97
 98      - name: Upload output
 99        if: always()
100        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
101        with:
102          name: assign-reviewers-output
103          path: /tmp/assign-reviewers-output.txt
104          retention-days: 30