Fix ext-apps: mcp-apps-wg should have push, not admin
This commit is contained in:
parent
d12dc886c6
commit
e36544e3af
5 changed files with 237 additions and 3 deletions
220
scripts/audit-discord-roles.ts
Normal file
220
scripts/audit-discord-roles.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/**
|
||||
* Script to audit Discord roles against the repo configuration.
|
||||
*
|
||||
* For each user defined in the repo with a Discord ID, this script:
|
||||
* 1. Gets their actual Discord roles
|
||||
* 2. Compares against expected roles based on their memberOf
|
||||
* 3. Reports discrepancies
|
||||
*
|
||||
* Run with: npx ts-node scripts/audit-discord-roles.ts
|
||||
*/
|
||||
|
||||
import { MEMBERS } from '../src/config/users';
|
||||
import { ROLES, getRole } from '../src/config/roles';
|
||||
import type { RoleId } from '../src/config/roleIds';
|
||||
|
||||
const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
|
||||
const GUILD_ID = process.env.DISCORD_GUILD_ID || '';
|
||||
|
||||
interface DiscordRole {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DiscordMember {
|
||||
user: { id: string; username: string; global_name: string };
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
// Build role name -> id lookup from Discord
|
||||
const discordRoleNameToId = new Map<string, string>();
|
||||
const discordRoleIdToName = new Map<string, string>();
|
||||
|
||||
// Build expected role name -> RoleId lookup from config
|
||||
const syncedRoleNameToRoleId = new Map<string, RoleId>();
|
||||
|
||||
async function fetchJson(url: string): Promise<any> {
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bot ${BOT_TOKEN}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Discord API error: ${response.status} ${error}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function getDiscordRoles(): Promise<DiscordRole[]> {
|
||||
return fetchJson(`https://discord.com/api/v10/guilds/${GUILD_ID}/roles`);
|
||||
}
|
||||
|
||||
async function getDiscordMember(discordId: string): Promise<DiscordMember | null> {
|
||||
try {
|
||||
return await fetchJson(`https://discord.com/api/v10/guilds/${GUILD_ID}/members/${discordId}`);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand role IDs to include all Discord-implied roles
|
||||
* (GitHub parent traversal + discordImplies)
|
||||
*/
|
||||
function expandDiscordRoles(roleIds: readonly RoleId[]): Set<RoleId> {
|
||||
const expanded = new Set<RoleId>();
|
||||
const queue = [...roleIds];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const roleId = queue.shift()!;
|
||||
if (expanded.has(roleId)) continue;
|
||||
|
||||
const role = getRole(roleId);
|
||||
if (!role) continue;
|
||||
|
||||
expanded.add(roleId);
|
||||
|
||||
// Add parent role (GitHub hierarchy)
|
||||
if (role.github?.parent) {
|
||||
queue.push(role.github.parent);
|
||||
}
|
||||
|
||||
// Add discord-implied roles
|
||||
if (role.discordImplies) {
|
||||
queue.push(...role.discordImplies);
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!BOT_TOKEN || !GUILD_ID) {
|
||||
console.error('Set DISCORD_BOT_TOKEN and DISCORD_GUILD_ID environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Fetching Discord roles...');
|
||||
const discordRoles = await getDiscordRoles();
|
||||
|
||||
// Build role lookups
|
||||
for (const role of discordRoles) {
|
||||
discordRoleNameToId.set(role.name, role.id);
|
||||
discordRoleIdToName.set(role.id, role.name);
|
||||
}
|
||||
|
||||
// Build synced role name -> RoleId lookup
|
||||
for (const role of ROLES) {
|
||||
if (role.discord?.role) {
|
||||
syncedRoleNameToRoleId.set(role.discord.role, role.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Map manual role names to their synced equivalents
|
||||
const manualToSynced = new Map<string, string>();
|
||||
for (const role of discordRoles) {
|
||||
const syncedName = `${role.name} (synced)`;
|
||||
if (discordRoleNameToId.has(syncedName)) {
|
||||
manualToSynced.set(role.name, syncedName);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Discord Roles Summary ===');
|
||||
console.log(`Total roles: ${discordRoles.length}`);
|
||||
console.log(`Synced roles: ${discordRoles.filter(r => r.name.endsWith('(synced)')).length}`);
|
||||
console.log(`Manual roles with synced equivalent: ${manualToSynced.size}`);
|
||||
|
||||
console.log('\n=== Auditing Users ===\n');
|
||||
|
||||
const issues: string[] = [];
|
||||
const membersWithDiscord = MEMBERS.filter(m => m.discord);
|
||||
|
||||
for (const member of membersWithDiscord) {
|
||||
const discordMember = await getDiscordMember(member.discord!);
|
||||
if (!discordMember) {
|
||||
issues.push(`❌ ${member.github || member.email}: Discord ID ${member.discord} not found in guild`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const username = discordMember.user.global_name || discordMember.user.username;
|
||||
|
||||
// Get actual role names
|
||||
const actualRoleNames = new Set(
|
||||
discordMember.roles.map(id => discordRoleIdToName.get(id) || id)
|
||||
);
|
||||
|
||||
// Get expected synced role names based on memberOf
|
||||
const expandedRoleIds = expandDiscordRoles(member.memberOf);
|
||||
const expectedSyncedRoles = new Set<string>();
|
||||
const expectedManualRoles = new Set<string>();
|
||||
|
||||
for (const roleId of expandedRoleIds) {
|
||||
const role = getRole(roleId);
|
||||
if (role?.discord?.role) {
|
||||
expectedSyncedRoles.add(role.discord.role);
|
||||
// Also expect the manual equivalent if it exists
|
||||
const manualName = role.discord.role.replace(' (synced)', '');
|
||||
if (discordRoleNameToId.has(manualName)) {
|
||||
expectedManualRoles.add(manualName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing synced roles
|
||||
const missingSyncedRoles: string[] = [];
|
||||
for (const expectedRole of expectedSyncedRoles) {
|
||||
if (!actualRoleNames.has(expectedRole)) {
|
||||
missingSyncedRoles.push(expectedRole);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for manual roles without synced equivalent
|
||||
const manualWithoutSynced: string[] = [];
|
||||
for (const roleName of actualRoleNames) {
|
||||
if (roleName.endsWith('(synced)')) continue;
|
||||
if (roleName === '@everyone') continue;
|
||||
|
||||
const syncedEquivalent = `${roleName} (synced)`;
|
||||
if (discordRoleNameToId.has(syncedEquivalent)) {
|
||||
// This is a manual role that has a synced equivalent
|
||||
if (!actualRoleNames.has(syncedEquivalent)) {
|
||||
manualWithoutSynced.push(`${roleName} (has no ${syncedEquivalent})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingSyncedRoles.length > 0 || manualWithoutSynced.length > 0) {
|
||||
console.log(`\n👤 ${username} (${member.github || member.email})`);
|
||||
console.log(` Discord ID: ${member.discord}`);
|
||||
|
||||
if (missingSyncedRoles.length > 0) {
|
||||
console.log(` ❌ Missing synced roles:`);
|
||||
for (const role of missingSyncedRoles) {
|
||||
console.log(` - ${role}`);
|
||||
issues.push(`${username}: Missing ${role}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (manualWithoutSynced.length > 0) {
|
||||
console.log(` ⚠️ Manual roles without synced equivalent:`);
|
||||
for (const role of manualWithoutSynced) {
|
||||
console.log(` - ${role}`);
|
||||
issues.push(`${username}: ${role}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`✅ ${username} (${member.github || member.email})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n\n=== Summary ===');
|
||||
if (issues.length === 0) {
|
||||
console.log('✅ All users have matching roles!');
|
||||
} else {
|
||||
console.log(`❌ Found ${issues.length} issues:\n`);
|
||||
for (const issue of issues) {
|
||||
console.log(` - ${issue}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
|
@ -313,7 +313,7 @@ export const REPOSITORY_ACCESS: RepositoryAccess[] = [
|
|||
teams: [
|
||||
{ team: 'core-maintainers', permission: 'push' },
|
||||
{ team: 'moderators', permission: 'triage' },
|
||||
{ team: 'mcp-apps-wg', permission: 'admin' },
|
||||
{ team: 'mcp-apps-wg', permission: 'push' },
|
||||
{ team: 'mcp-apps-sdk', permission: 'admin' },
|
||||
],
|
||||
users: [
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const ROLE_IDS = {
|
|||
LEAD_MAINTAINERS: 'lead-maintainers',
|
||||
MODERATORS: 'moderators',
|
||||
ADMINISTRATORS: 'administrators', // Discord only
|
||||
COMMUNITY_MANAGERS: 'community-managers', // Discord only
|
||||
|
||||
// ===================
|
||||
// Maintainer Groups
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@ export const ROLES: readonly Role[] = [
|
|||
discord: { role: 'administrators (synced)' },
|
||||
// Discord only - no GitHub equivalent
|
||||
},
|
||||
{
|
||||
id: ROLE_IDS.COMMUNITY_MANAGERS,
|
||||
description: 'Discord community managers',
|
||||
discord: { role: 'community managers (synced)' },
|
||||
// Discord only - no GitHub equivalent
|
||||
},
|
||||
{
|
||||
id: ROLE_IDS.LEAD_MAINTAINERS,
|
||||
description: 'Lead core maintainers',
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export const MEMBERS: readonly Member[] = [
|
|||
email: 'cliff@futurescale.com',
|
||||
discord: '501498061965754380',
|
||||
memberOf: [
|
||||
ROLE_IDS.COMMUNITY_MANAGERS,
|
||||
ROLE_IDS.MAINTAINERS,
|
||||
ROLE_IDS.DOCS_MAINTAINERS,
|
||||
ROLE_IDS.INSPECTOR_MAINTAINERS,
|
||||
|
|
@ -163,7 +164,7 @@ export const MEMBERS: readonly Member[] = [
|
|||
{
|
||||
github: 'evalstate',
|
||||
discord: '779268016121577492',
|
||||
memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.MODERATORS],
|
||||
memberOf: [ROLE_IDS.COMMUNITY_MANAGERS, ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.MODERATORS],
|
||||
},
|
||||
{
|
||||
github: 'fabpot',
|
||||
|
|
@ -271,6 +272,11 @@ export const MEMBERS: readonly Member[] = [
|
|||
discord: '936242781733654588',
|
||||
memberOf: [ROLE_IDS.GO_SDK],
|
||||
},
|
||||
{
|
||||
github: 'macoughl',
|
||||
discord: '740279257548193803',
|
||||
memberOf: [ROLE_IDS.COMMUNITY_MANAGERS],
|
||||
},
|
||||
{
|
||||
github: 'maheshmurag',
|
||||
memberOf: [ROLE_IDS.MODERATORS],
|
||||
|
|
@ -334,7 +340,7 @@ export const MEMBERS: readonly Member[] = [
|
|||
{
|
||||
github: 'olaservo',
|
||||
discord: '1079841769946095620',
|
||||
memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.INSPECTOR_MAINTAINERS, ROLE_IDS.MODERATORS],
|
||||
memberOf: [ROLE_IDS.COMMUNITY_MANAGERS, ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.INSPECTOR_MAINTAINERS, ROLE_IDS.MODERATORS],
|
||||
},
|
||||
{
|
||||
github: 'pcarleton',
|
||||
|
|
@ -399,6 +405,7 @@ export const MEMBERS: readonly Member[] = [
|
|||
discord: '400092503677599754',
|
||||
email: 'tadas@modelcontextprotocol.io',
|
||||
memberOf: [
|
||||
ROLE_IDS.COMMUNITY_MANAGERS,
|
||||
ROLE_IDS.MODERATORS,
|
||||
ROLE_IDS.MAINTAINERS,
|
||||
ROLE_IDS.WORKING_GROUPS,
|
||||
|
|
|
|||
Loading…
Reference in a new issue