Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 68 additions & 101 deletions .github/workflows/pr-approval-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,128 +2,95 @@ name: PR Approval Check

on:
pull_request_review:
types: [submitted]
types: [submitted, dismissed]
pull_request:
types: [opened, reopened, synchronize]

jobs:
check-product-eng-approval:
name: Check Product Eng Approval
runs-on: ubuntu-latest

permissions:
statuses: write
pull-requests: write
steps:
- name: Check for Product Engineering approval
uses: actions/github-script@v7
env:
ORG: trufflesecurity
APPROVER_TEAM: product-eng
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# github-token: ${{ secrets.GH_REVIEW_TOKEN }}
script: |
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});

console.log(`Found ${reviews.length} reviews`);

// Get approved reviews
const approvals = reviews.filter(review => review.state === 'APPROVED');
console.log(`Found ${approvals.length} approvals`);

if (approvals.length === 0) {
console.log('No approvals found');
const { ORG: org, APPROVER_TEAM: team} = process.env
const prettyTeamName = `@${org}/${team}`

async function status(state, msg) {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.pull_request.head.sha,
state: 'pending',
context: 'product-eng-approval',
description: 'Waiting for approval from @trufflesecurity/product-eng team member'
state: state,
context: `product-eng-approval-${team}`,
description: msg,
});
}

async function fail(msg) {
core.setFailed(msg);
console.error(msg)
await status('failure', msg)
process.exit(1);
}

async function succeed(msg) {
core.setOutput(msg);
console.info(msg)
await status('success', msg)
process.exit(0);
}

await status('pending', `Waiting for approval from ${prettyTeamName} team member`)

let latestReviews = {};
try {
const iter = octokit.paginate.iterator(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
return;
let pages = [];
for await (const p of iter) {
pages.push(p)
}

const laterThan = (a, b) => new Date(a.submitted_at) > new Date(b.submitted_at) ? a : b;
latestReviews = pages.reduce((acc, page) => page.data.reduce((a, r) => ((!(r.user.login in a) || laterThan(r, a[r.user.login]) ? { ...a, [r.user.login]: r } : a)), acc) , {})
} catch (error) {
await fail(`⚠️ Could not get reviews: ${error.status}`);
}
// Helper function to get all teams to check (parent + children)
async function getTeamsToCheck() {
const teamsToCheck = ['product-eng']; // Start with parent team


for (const reviewer in latestReviews) {
if (latestReviews[reviewer].state !== 'APPROVED') {
continue
}
try {
// Get child teams of product-eng
const { data: childTeams } = await github.rest.teams.listChildInOrg({
org: 'trufflesecurity',
team_slug: 'product-eng'
});

// Add child team slugs
childTeams.forEach(team => {
teamsToCheck.push(team.slug);
const membership = await github.rest.teams.getMembershipForUserInOrg({
org: org,
team_slug: team,
username: reviewer,
});

console.log(`Teams to check: ${teamsToCheck.join(', ')}`);

if (membership.data.state == 'active') {
await succeed(`✅ Approved by ${reviewer} on behalf of ${prettyTeamName}`)
}
} catch (error) {
console.log('Error fetching child teams, will only check parent team:', error.message);
}

return teamsToCheck;
}

// Helper function to check if user is member of any team
async function isUserMemberOfAnyTeam(username, teams) {
for (const teamSlug of teams) {
try {
const { data: membership } = await github.rest.teams.getMembershipForUserInOrg({
org: 'trufflesecurity',
team_slug: teamSlug,
username: username
});

if (membership.state === 'active') {
console.log(`✅ Found active member of @trufflesecurity/${teamSlug}: ${username}`);
return { isMember: true, teamSlug };
}
} catch (error) {
// User is not a member of this team (404) or other error
console.log(`❌ ${username} is not a member of @trufflesecurity/${teamSlug}`);
if (error.status != 404) {
await fail(`⚠️ Could not determine membership for ${reviewer} in ${prettyTeamName}: ${error.status}`)
}
}
return { isMember: false, teamSlug: null };
}

// Get all teams to check (parent + children)
const teamsToCheck = await getTeamsToCheck();

// Check if any approver is a member of product-eng or its child teams
let hasProductEngApproval = false;
let approverInfo = null;

for (const approval of approvals) {
const membershipCheck = await isUserMemberOfAnyTeam(approval.user.login, teamsToCheck);

if (membershipCheck.isMember) {
hasProductEngApproval = true;
approverInfo = {
username: approval.user.login,
teamSlug: membershipCheck.teamSlug
};
break;
}
}

if (hasProductEngApproval) {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.pull_request.head.sha,
state: 'success',
context: 'product-eng-approval',
description: `✅ Approved by @trufflesecurity/${approverInfo.teamSlug} member (${approverInfo.username})`
});
} else {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.pull_request.head.sha,
state: 'failure',
context: 'product-eng-approval',
description: '❌ Requires approval from @trufflesecurity/product-eng team member'
});
}

await fail(`⚠️ No approvers found from ${prettyTeamName}`)


Loading