Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 43 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# GitHub Workflows

This directory contains GitHub Actions workflows for the TruffleHog repository.

## PR Approval Check (`pr-approval-check.yml`)

This workflow enforces that at least one PR approver must be a member of the `@trufflesecurity/product-eng` team or any of its child teams.

### How it works:

1. **Triggers**: The workflow runs on:
- `pull_request_review` events when a review is submitted
- `pull_request` events when a PR is opened, reopened, or synchronized

2. **Approval Check**: The workflow:
- Fetches all reviews for the PR
- Filters for approved reviews
- Gets all child teams of `@trufflesecurity/product-eng`
- Checks if any approver is an active member of the parent team or any child team
- Sets a commit status accordingly

3. **Status Check**: Creates a commit status named `product-eng-approval` with:
- ✅ **Success**: When at least one approver is a `@trufflesecurity/product-eng` or child team member
- ❌ **Failure**: When no `@trufflesecurity/product-eng` or child team members have approved
- ⏳ **Pending**: When waiting for reviews

### Branch Protection

To make this check required:

1. Go to Settings → Branches
2. Add or edit a branch protection rule for your main branch
3. Enable "Require status checks to pass before merging"
4. Add `product-eng-approval` to the required status checks

### Permissions

The workflow uses the default `GITHUB_TOKEN` which has sufficient permissions to:
- Read PR reviews
- Check team membership (for public teams)
- Create commit statuses

**Note**: If the `product-eng` team or its child teams are private, you may need to use a personal access token with appropriate permissions.
129 changes: 129 additions & 0 deletions .github/workflows/pr-approval-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: PR Approval Check

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

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

steps:
- name: Check for Product Engineering approval
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_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');
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'
});
return;
}

// Helper function to get all teams to check (parent + children)
async function getTeamsToCheck() {
const teamsToCheck = ['product-eng']; // Start with parent team

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);
});

console.log(`Teams to check: ${teamsToCheck.join(', ')}`);
} 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}`);
}
}
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'
});
}
Loading