Skip to content

Instantly share code, notes, and snippets.

@sean-brydon
Created November 17, 2025 14:25
Show Gist options
  • Select an option

  • Save sean-brydon/6934f24415093642b7610603097cc07f to your computer and use it in GitHub Desktop.

Select an option

Save sean-brydon/6934f24415093642b7610603097cc07f to your computer and use it in GitHub Desktop.
weighted approvals
name: Weighted approvals
on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review]
pull_request_review:
types: [submitted, edited, dismissed]
permissions:
pull-requests: read
contents: read
# If you need to read org team membership for private orgs, you may need:
# organizations: read
jobs:
gate:
runs-on: ubuntu-latest
steps:
- name: Evaluate weighted approvals
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
// SETTINGS
// Teams are declared as "org/team-slug" (same format as @org/team mentions)
// Each team listed will have +2 weight per member approval.
const TEAMS_WITH_PLUS_TWO_APPROVALS = [
'calcom/foundation',
// 'calcom/security',
// 'another-org/senior-reviewers',
];
const REQUIRED_WEIGHTED_SCORE = 2; // threshold to pass (e.g., emulate “+2”)
const EXCLUDE_AUTHOR = true; // set false to allow PR author’s approval to count
// Helper: group teams by org
const teamsByOrg = TEAMS_WITH_PLUS_TWO_APPROVALS.reduce((acc, t) => {
const [org, teamSlug] = t.split('/');
if (!org || !teamSlug) {
throw new Error(`Invalid team format: ${t}. Use "org/team-slug".`);
}
acc[org] = acc[org] || new Set();
acc[org].add(teamSlug);
return acc;
}, {});
// Fetch PR
const pr = context.payload.pull_request || (await github.rest.pulls.get({
owner, repo, pull_number: context.issue.number
})).data;
const prNumber = pr.number;
// Fetch all reviews and determine latest state per user
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner, repo, pull_number: prNumber, per_page: 100
});
const sorted = [...reviews].sort((a,b) => new Date(a.submitted_at) - new Date(b.submitted_at));
const latestStateByUser = new Map();
const userById = new Map();
for (const r of sorted) {
if (r.user?.type === 'Bot') continue;
latestStateByUser.set(r.user.id, r.state);
userById.set(r.user.id, r.user);
}
// Collect latest APPROVED reviewers
let approvers = [];
for (const [uid, state] of latestStateByUser.entries()) {
if (state === 'APPROVED') {
const u = userById.get(uid);
approvers.push(u);
}
}
// Optionally exclude PR author
if (EXCLUDE_AUTHOR) {
approvers = approvers.filter(u => u.login !== pr.user.login);
}
// Build a cache of team memberships across all configured orgs/teams
// For each org, union all members from the listed team slugs
const plusTwoMembers = new Set();
for (const [org, teamSlugsSet] of Object.entries(teamsByOrg)) {
for (const teamSlug of teamSlugsSet) {
// Requires org visibility for Actions token; for private orgs you may need a PAT with read:org
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
org,
team_slug: teamSlug,
per_page: 100
});
for (const m of members) {
plusTwoMembers.add(`${org}///${m.login.toLowerCase()}`);
}
}
}
// Compute weighted score
let score = 0;
const details = [];
for (const u of approvers) {
// Determine user org context for membership check:
// We can’t know a user's org “ownership” directly, so we check them against all orgs we queried.
// We mark +2 if user login is in any of the org/team membership sets we collected.
const isPlusTwo = [...Object.keys(teamsByOrg)].some(org => plusTwoMembers.has(`${org}///${u.login.toLowerCase()}`));
score += isPlusTwo ? 2 : 1;
details.push(`${u.login}${isPlusTwo ? ' (x2 team)' : ''}`);
}
const ok = score >= REQUIRED_WEIGHTED_SCORE;
// Summary output
core.summary
.addHeading('Weighted approvals')
.addRaw(`Teams (x2): ${TEAMS_WITH_PLUS_TWO_APPROVALS.join(', ') || 'none'}\n`)
.addRaw(`Approvers: ${details.join(', ') || 'none'}\n`)
.addRaw(`Weighted score: ${score} / ${REQUIRED_WEIGHTED_SCORE}\n`)
.write();
if (!ok) {
core.setFailed(`Need weighted score ≥ ${REQUIRED_WEIGHTED_SCORE}. Current: ${score}.`);
} else {
core.notice('Weighted approval requirement satisfied.');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment