Skip to content

Commit 765392d

Browse files
Merge branch 'main' into feature/oss-209
2 parents 0c4cda5 + e473452 commit 765392d

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

.github/workflows/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# GitHub Workflows
2+
3+
This directory contains GitHub Actions workflows for the TruffleHog repository.
4+
5+
## PR Approval Check (`pr-approval-check.yml`)
6+
7+
This workflow enforces that at least one PR approver must be an **active** member of the `@trufflesecurity/product-eng` team or any of its child teams.
8+
9+
### How it works:
10+
11+
1. **Triggers**: The workflow runs on:
12+
- `pull_request_review` events when a review is submitted (`submitted` type)
13+
- `pull_request` events when a PR is opened, reopened, or synchronized (`opened`, `reopened`, `synchronize` types)
14+
15+
2. **Approval Check Process**: The workflow:
16+
- Fetches all reviews for the PR using the GitHub API
17+
- Filters for reviews with state `APPROVED`
18+
- Gets all child teams of `@trufflesecurity/product-eng` using `listChildInOrg` API
19+
- Checks if any approver is an **active** member (not pending) of either:
20+
- The parent `@trufflesecurity/product-eng` team, OR
21+
- Any of its child teams
22+
- Sets a commit status accordingly
23+
24+
3. **Status Check**: Creates a commit status named `product-eng-approval` with:
25+
-**Success**: When at least one approver is an active member of `@trufflesecurity/product-eng` or any child team
26+
-**Failure**: When there are no approvals or there are approvals but none from active `@trufflesecurity/product-eng` members
27+
28+
### Error Handling
29+
30+
If there are errors listing reviews or checking team membership, the workflow reports a failure status and also fails itself.
31+
32+
### Branch Protection
33+
34+
To make this check required:
35+
36+
1. Go to Settings → Branches
37+
2. Add or edit a branch protection rule for your main branch
38+
3. Enable "Require status checks to pass before merging"
39+
4. Add `pr-approval-check` to the required status checks
40+
41+
### Permissions
42+
43+
The workflow uses the default `GITHUB_TOKEN` which has sufficient permissions to:
44+
- Read PR reviews
45+
- List child teams and check team membership (for public teams)
46+
- Create commit statuses
47+
48+
**Note**: If the `product-eng` team or its child teams are private, you may need to use a personal access token with appropriate permissions. The Github API returns 404 for non-members and for lack of permissions.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: PR Approval Check
2+
3+
on:
4+
pull_request_review:
5+
types: [submitted, dismissed]
6+
pull_request_target:
7+
types: [opened, reopened, synchronize]
8+
9+
jobs:
10+
check-product-eng-approval:
11+
name: Check Product Eng Approval
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Check for Product Engineering approval
15+
uses: actions/github-script@v7
16+
env:
17+
ORG: trufflesecurity
18+
APPROVER_TEAM: product-eng
19+
with:
20+
github-token: ${{ secrets.PR_APPROVAL_CHECK }}
21+
script: |
22+
const { ORG: org, APPROVER_TEAM: team} = process.env
23+
const prettyTeamName = `@${org}/${team}`
24+
25+
async function status(state, msg) {
26+
await github.rest.repos.createCommitStatus({
27+
owner: context.repo.owner,
28+
repo: context.repo.repo,
29+
sha: context.payload.pull_request.head.sha,
30+
state: state,
31+
context: 'pr-approval-check',
32+
description: msg,
33+
});
34+
}
35+
36+
async function fail(msg) {
37+
core.setOutput(msg);
38+
console.error(msg)
39+
await status('failure', msg)
40+
process.exit(0);
41+
}
42+
43+
async function succeed(msg) {
44+
core.setOutput(msg);
45+
console.info(msg)
46+
await status('success', msg)
47+
process.exit(0);
48+
}
49+
50+
async function fatal(msg) {
51+
core.setFailed(msg);
52+
console.error(msg)
53+
await status('failure', msg)
54+
process.exit(1);
55+
}
56+
57+
58+
await status('pending', `Waiting for approval from ${prettyTeamName} team member`)
59+
60+
// reviews maps reviewer logins to their latest review
61+
let reviews = new Map();
62+
try {
63+
const iter = octokit.paginate.iterator(github.rest.pulls.listReviews, {
64+
owner: context.repo.owner,
65+
repo: context.repo.repo,
66+
pull_number: context.payload.pull_request.number,
67+
});
68+
69+
let pages = [];
70+
for await (const page of iter) {
71+
pages.push(page)
72+
}
73+
74+
const latestReview = (a, b) => new Date(a.submitted_at) > new Date(b.submitted_at) ? a : b;
75+
for await (const page of pages) {
76+
for (const review of page.data) {
77+
const login = review.user.login;
78+
reviews.set(login, reviews.has(login) ? latestReview(reviews.get(login), review) : review);
79+
}
80+
}
81+
} catch (error) {
82+
await fatal(`⚠️ Could not get reviews: ${error.status}`);
83+
}
84+
85+
let approved = false;
86+
let changeRequesters = [];
87+
for (const [login, review] of reviews) {
88+
try {
89+
const membership = await github.rest.teams.getMembershipForUserInOrg({
90+
org: org,
91+
team_slug: team,
92+
username: login,
93+
});
94+
95+
if (membership.data.state === 'active') {
96+
if (review.state === 'APPROVED') {
97+
approved = true;
98+
} else if (review.state === 'CHANGES_REQUESTED') {
99+
changeRequesters.push(login);
100+
}
101+
}
102+
103+
} catch (error) {
104+
if (error.status != 404) {
105+
await fatal(`⚠️ Could not determine membership for ${login} in ${prettyTeamName}: ${error.status}`)
106+
}
107+
}
108+
}
109+
110+
if (changeRequesters.length > 0) {
111+
await fail(`⚠️ Changes requested by: ${changeRequesters.map(login=>`@${login}`).join(", ")} on behalf of ${prettyTeamName}`);
112+
} else if (approved) {
113+
await succeed(`✅ Approved by ${prettyTeamName}`)
114+
} else {
115+
await fail(`⚠️ Requires approval from ${prettyTeamName}`)
116+
}
117+

0 commit comments

Comments
 (0)