Skip to content

Check PR Approvals #1484

Check PR Approvals

Check PR Approvals #1484

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 the list of changed files
const { data: changedFiles } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number,
});
// Check if any files in doc/, library/ or verifast-proofs/ are modified
const affectsDocs = changedFiles.some(file => file.filename.startsWith('doc/'));
const affectsLibrary = changedFiles.some(file => file.filename.startsWith('library/'));
const affectsVerifast = changedFiles.some(file => file.filename.startsWith('verifast-proofs/'));
// Require two approvals iff one of the above folders are modified; otherwise, one is sufficient.
const requiresTwoApprovals = affectsDocs || affectsLibrary || affectsVerifast;
const requiredApprovals = requiresTwoApprovals ? 2 : 1;
// 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 committeeApprovers = Array.from(approvers)
.filter(approver => requiredApprovers.includes(approver));
const currentCountfromCommittee = committeeApprovers.length;
// Check if we have enough approvals
const conclusion = (currentCountfromCommittee >= requiredApprovals) ? 'success' : 'failure';
console.log('PR Approval Status');
console.log('Modified folders:');
console.log(`- doc/: ${affectsDocs ? 'yes' : 'no'}`);
console.log(`- library/: ${affectsLibrary ? 'yes' : 'no'}`);
console.log(`- verifast-proofs/: ${affectsVerifast ? 'yes' : 'no'}`);
console.log(`Required approvals from committee: ${requiredApprovals}`);
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 ${requiredApprovals} approval${requiredApprovals > 1 ? 's' : ''} from committee members, but it has ${currentCountfromCommittee}`);
} else {
core.info('PR approval check passed successfully.');
}