Skip to content

Check PR Approvals #1080

Check PR Approvals

Check PR Approvals #1080

Workflow file for this run

# This workflow checks that the PR has been approved by 2+ members of the committee listed in `pull_requests.toml`.
#
# Run this action when a pull request review is submitted / dismissed.
# Note that an approval can be dismissed, and this can affect the job status.
# We currently trust that contributors won't make significant changes to their PRs after approval, so we accept
# changes after approval.
#
# We still need to run this in the case of a merge group, since it is a required step. In that case, the workflow
# is a NOP.
name: Check PR Approvals
on:
merge_group:
pull_request_review:
types: [submitted, dismissed]
jobs:
check-approvals:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
if: ${{ github.event_name != 'merge_group' }}
- name: Install TOML parser
run: npm install @iarna/toml
if: ${{ github.event_name != 'merge_group' }}
- name: Check PR Relevance and Approvals
uses: actions/github-script@v6
if: ${{ github.event_name != 'merge_group' }}
with:
script: |
const fs = require('fs');
const toml = require('@iarna/toml');
const { owner, repo } = context.repo;
let pull_number;
if (github.event_name === 'workflow_dispatch') {
const branch = github.ref.replace('refs/heads/', '');
const prs = await github.rest.pulls.list({
owner,
repo,
head: `${owner}:${branch}`,
state: 'open'
});
if (prs.data.length === 0) {
console.log('No open PR found for this branch.');
return;
}
pull_number = prs.data[0].number;
} else {
pull_number = context.issue.number;
}
console.log(`owner is ${owner}`);
console.log(`pull_number is ${pull_number}`);
// Get parsed data
let requiredApprovers;
try {
const tomlContent = fs.readFileSync('.github/pull_requests.toml', 'utf8');
console.log('TOML content:', tomlContent);
const tomlData = toml.parse(tomlContent);
console.log('Parsed TOML data:', JSON.stringify(tomlData, null, 2));
if (!tomlData.committee || !Array.isArray(tomlData.committee.members)) {
throw new Error('committee.members is not an array in the TOML file');
}
requiredApprovers = tomlData.committee.members;
} catch (error) {
console.error('Error reading or parsing TOML file:', error);
core.setFailed('Failed to read required approvers list');
return;
}
// Get all reviews with pagination
async function getAllReviews() {
let allReviews = [];
let page = 1;
let page_limit = 100;
while (page < page_limit) {
const response = await github.rest.pulls.listReviews({
owner,
repo,
pull_number,
per_page: 100,
page
});
allReviews = allReviews.concat(response.data);
if (response.data.length < 100) {
break;
}
page++;
}
if (page == page_limit) {
console.log(`WARNING: Reached page limit of ${page_limit} while fetching reviews data; approvals count may be less than the real total.`)
}
return allReviews;
}
const reviews = await getAllReviews();
// Example: approvers = ["celina", "zyad"]
const approvers = new Set(
reviews
.filter(review => review.state === 'APPROVED')
.map(review => review.user.login)
);
const requiredApprovals = 2;
const committeeApprovers = Array.from(approvers)
.filter(approver => requiredApprovers.includes(approver));
const currentCountfromCommittee = committeeApprovers.length;
// Core logic that checks if the approvers are in the committee
const conclusion = (currentCountfromCommittee >= 2) ? 'success' : 'failure';
console.log('PR Approval Status');
console.log(`PR has ${approvers.size} total approvals and ${currentCountfromCommittee} required approvals from the committee.`);
console.log(`Committee Members: [${requiredApprovers.join(', ')}]`);
console.log(`Committee Approvers: ${committeeApprovers.length === 0 ? 'NONE' : `[${committeeApprovers.join(', ')}]`}`);
console.log(`All Approvers: ${approvers.size === 0 ? 'NONE' : `[${Array.from(approvers).join(', ')}]`}`);
if (conclusion === 'failure') {
core.setFailed(`PR needs 2 approvals from committee members, but it has ${currentCountfromCommittee}`);
} else {
core.info('PR approval check passed successfully.');
}