name: Check Approvals on: pull_request: types: [opened, synchronize, ready_for_review] pull_request_review: types: [submitted] jobs: check_owners: name: Check OWNERS approval runs-on: ubuntu-latest # Skip draft PRs. if: github.event.pull_request.draft == false steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 # We don't actually need the contents of the files, just their names. filter: blob:none - name: Get changed files id: changed_files run: | HEAD_SHA=${{ github.event.pull_request.head.sha }} BASE_SHA=${{ github.event.pull_request.base.sha }} MERGE_BASE=$(git merge-base "$BASE_SHA" "$HEAD_SHA") CHANGED_FILES=$(git diff --name-only "$MERGE_BASE" "$HEAD_SHA" | tr '\n' ' ') echo "files=$CHANGED_FILES" >> "$GITHUB_OUTPUT" - name: Get PR approvers id: approvers env: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER=${{ github.event.pull_request.number }} PR_AUTHOR=${{ github.event.pull_request.user.login }} # Get all approved reviews. REVIEWERS=$(gh pr view "$PR_NUMBER" --json reviews --jq '[.reviews[] | select(.state == "APPROVED") | .author.login] | unique | join(" ")') # Combine PR author (self-approves) with reviewers. ALL_APPROVERS="$PR_AUTHOR $REVIEWERS" echo "approvers=$ALL_APPROVERS" >> "$GITHUB_OUTPUT" echo "has_reviewers=$( [[ -n "$REVIEWERS" ]] && echo true || echo false )" >> "$GITHUB_OUTPUT" echo "PR author: $PR_AUTHOR" echo "Reviewers who approved: $REVIEWERS" echo "All approvers: $ALL_APPROVERS" - name: Check OWNERS approval env: CHANGED_FILES: ${{ steps.changed_files.outputs.files }} APPROVERS: ${{ steps.approvers.outputs.approvers }} run: | set -e # Convert approvers to an array. read -ra APPROVER_ARRAY <<< "$APPROVERS" # Function to get owners for a file by walking up the directory tree. get_owners_for_file() { local file="$1" local dir dir=$(dirname "$file") local owners=() # Walk up the directory tree. while [[ "$dir" != "." && "$dir" != "/" ]]; do if [[ -f "$dir/OWNERS" ]]; then # Read owners file, skip comments and empty lines. while IFS= read -r line || [[ -n "$line" ]]; do # Skip comments and empty lines. line=$(echo "$line" | sed 's/#.*//' | xargs) if [[ -n "$line" ]]; then owners+=("$line") fi done < "$dir/OWNERS" fi dir=$(dirname "$dir") done # Check root directory. if [[ -f "OWNERS" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do line=$(echo "$line" | sed 's/#.*//' | xargs) if [[ -n "$line" ]]; then owners+=("$line") fi done < "OWNERS" fi # Return unique owners. printf '%s\n' "${owners[@]}" | sort -u | tr '\n' ' ' | sed 's/ $//' } # Function to check if any approver is in the owners list. has_owner_approval() { local owners="$1" for approver in "${APPROVER_ARRAY[@]}"; do if echo "$owners" | grep -qw "$approver"; then return 0 fi done return 1 } MISSING_APPROVAL=() FILES_CHECKED=0 for file in $CHANGED_FILES; do # Skip if file doesn't exist (deleted files). FILES_CHECKED=$((FILES_CHECKED + 1)) OWNERS=$(get_owners_for_file "$file") if [[ -z "$OWNERS" ]]; then # No OWNERS files in ancestry - automatically passes. echo "✓ $file (no OWNERS requirement)" continue fi if has_owner_approval "$OWNERS"; then echo "✓ $file" else echo "✗ $file (owners: $OWNERS)" MISSING_APPROVAL+=("$file") fi done echo "" echo "Files checked: $FILES_CHECKED" echo "Approvers: ${APPROVER_ARRAY[*]}" # Save missing files for comment step (must happen before exit). echo "MISSING_FILES<> "$GITHUB_ENV" for file in "${MISSING_APPROVAL[@]}"; do OWNERS=$(get_owners_for_file "$file") echo "- \`$file\` (needs approval from: $OWNERS)" >> "$GITHUB_ENV" done echo "EOF" >> "$GITHUB_ENV" if [[ ${#MISSING_APPROVAL[@]} -gt 0 ]]; then echo "" echo "::error::The following files are missing OWNERS approval:" for file in "${MISSING_APPROVAL[@]}"; do OWNERS=$(get_owners_for_file "$file") echo "::error:: - $file (needs approval from: $OWNERS)" done exit 1 fi echo "" echo "All files have required OWNERS approval." - name: Comment on PR about missing approvals if: failure() && github.event_name == 'pull_request_review' && steps.approvers.outputs.has_reviewers == 'true' env: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER=${{ github.event.pull_request.number }} gh pr comment "$PR_NUMBER" --body "The following files still need approval from their OWNERS:\n\n$MISSING_FILES"