Skip to content

Add unified platform map for cross-platform API documentation #427

Add unified platform map for cross-platform API documentation

Add unified platform map for cross-platform API documentation #427

name: Backward Incompatibility Reviewer Check
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted]
jobs:
check-backward-incompatibilities-reviewers:
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch (sparse - only .DevConfigs)
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
sparse-checkout: |
generator/.DevConfigs
sparse-checkout-cone-mode: false
- name: Check backwardIncompatibilitiesToIgnore and reviewers
uses: actions/github-script@v7.0.1
with:
script: |
const fs = require('fs');
const path = require('path');
// list of required reviewers
const REQUIRED_REVIEWERS = ['normj', 'boblodgett'];
function findDevConfigFiles() {
const devConfigPath = 'generator/.DevConfigs';
try {
if (!fs.existsSync(devConfigPath)) {
console.log('No .DevConfigs directory found');
return [];
}
const files = fs.readdirSync(devConfigPath);
const jsonFiles = files
.filter(file => file.endsWith('.json'))
.map(file => path.join(devConfigPath, file));
return jsonFiles;
} catch (error) {
console.log('Error accessing .DevConfigs directory:', error.message);
return [];
}
}
function hasBackwardIncompatibilities(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const jsonData = JSON.parse(content);
// Check if any key in the JSON contains backwardIncompatibilitiesToIgnore
function searchForKey(obj) {
if (typeof obj !== 'object' || obj === null) return false;
for (const key in obj) {
if (key === 'backwardIncompatibilitiesToIgnore') {
return true;
}
if (typeof obj[key] === 'object' && searchForKey(obj[key])) {
return true;
}
}
return false;
}
return searchForKey(jsonData);
} catch (error) {
console.log(`Error reading or parsing ${filePath}:`, error.message);
return false;
}
}
// Find all DevConfig files
const devConfigFiles = findDevConfigFiles();
console.log(`Found ${devConfigFiles.length} DevConfig files in PR branch`);
if (devConfigFiles.length === 0) {
console.log('No DevConfig files found, skipping backward incompatibility check');
return;
}
// Check if any file has backwardIncompatibilitiesToIgnore
let foundBackwardIncompatibilities = false;
const filesWithIncompatibilities = [];
for (const file of devConfigFiles) {
console.log(`Checking file: ${file}`);
if (hasBackwardIncompatibilities(file)) {
foundBackwardIncompatibilities = true;
filesWithIncompatibilities.push(file);
console.log(`Found backwardIncompatibilitiesToIgnore in: ${file}`);
}
}
if (!foundBackwardIncompatibilities) {
console.log('No backward incompatibilities to ignore found in DevConfig files');
return;
}
// If backwardIncompatibilitiesToIgnore found, handle based on event type
const isReviewEvent = context.eventName === 'pull_request_review';
const message = `Backward compatibility review required - approval needed from one of the reviewers: ${REQUIRED_REVIEWERS.join(', ')}`;
if (isReviewEvent) {
// For review events, check for required reviewer approvals and enforce
console.log('Review event - checking for required reviewer approvals...');
let approvedRequiredReviewers = [];
try {
// Get PR reviews to check for approvals
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
// Check review requests (pending reviews)
let pendingReviewers = [];
try {
const { data: reviewRequests } = await github.rest.pulls.listRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
pendingReviewers = reviewRequests.users
.filter(user => REQUIRED_REVIEWERS.includes(user.login))
.map(user => user.login);
} catch (requestError) {
console.log('Could not fetch review requests:', requestError.message);
}
// Find the latest review state for each reviewer
const latestReviewsByUser = {};
let hasAnyRequiredReviews = false;
for (const reviewer of REQUIRED_REVIEWERS) {
const userReviews = reviews
.filter(review => review.user && review.user.login === reviewer)
.sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at)) // Descending by DATE
.slice(0, 1); // Take first 1 (most recent)
if (userReviews.length > 0) {
hasAnyRequiredReviews = true;
let reviewState = userReviews[0].state;
console.log(`Latest review for ${reviewer}: ${reviewState} at ${userReviews[0].submitted_at}`);
// If reviewer has pending request AND approved review, their approval was reset
if (reviewState === 'APPROVED' && pendingReviewers.includes(reviewer)) {
reviewState = 'RESET_BY_REQUEST';
console.log(`${reviewer} approval reset by re-request - now pending review`);
}
latestReviewsByUser[reviewer] = reviewState;
} else {
console.log(`No reviews found for ${reviewer}`);
}
}
// Early exit if no required reviewers have submitted any reviews
if (!hasAnyRequiredReviews) {
console.log('No reviews found from any required reviewers - failing check');
core.setFailed(message);
return;
}
// Check if at least one required reviewer has approved
approvedRequiredReviewers = REQUIRED_REVIEWERS.filter(reviewer =>
latestReviewsByUser[reviewer] === 'APPROVED'
);
console.log('Required reviewers:', REQUIRED_REVIEWERS.join(', '));
console.log('Reviewers who have approved:', approvedRequiredReviewers.join(', ') || 'none');
if (approvedRequiredReviewers.length === 0) {
// No required reviewer has approved, fail the check
core.setFailed(message);
} else {
console.log('Required reviewers have approved, backward incompatibility check passed');
}
} catch (error) {
console.log('Error checking reviews:', error.message);
// For review events, fail securely - don't allow merge without verification
core.setFailed(`Unable to verify reviewer approvals due to API error: ${error.message}`);
}
} else {
// For PR events, only handle comment creation (ignore reviewer status)
console.log('PR event - checking for existing comment...');
const commentSignature = '<!-- BACKWARD_COMPATIBILITY_CHECK -->';
try {
// Check if comment already exists
const { data: existingComments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existingComment = existingComments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes(commentSignature)
);
if (existingComment) {
console.log('Backward compatibility comment already exists, skipping creation');
} else {
// Create comment only if it doesn't exist
const comment = `${commentSignature} ${message}`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
console.log('Successfully created backward compatibility comment');
}
} catch (commentError) {
// Don't fail PR workflows for comment errors, just log warning
console.log('Warning: Failed to create comment:', commentError.message);
}
}