#!/usr/bin/env npx ts-node /** * Validates that all references in the config are valid. * Run with: npx ts-node scripts/validate-config.ts */ import { ROLES, buildRoleLookup } from '../src/config/roles'; import { REPOSITORY_ACCESS } from '../src/config/repoAccess'; import { NPM_PACKAGES, PYPI_PROJECTS, UNMAPPED_NPM_USERS, UNMAPPED_PYPI_USERS, } from '../src/config/packageAccess'; import { MEMBERS } from '../src/config/users'; import { hasProvisionUserRole, resolveGoogleMemberEmail } from '../src/config/utils'; import type { RoleId } from '../src/config/roleIds'; const roleLookup = buildRoleLookup(); // Get all GitHub team names (roles that have GitHub config) const githubTeamNames = new Set(); // Get all role IDs (for member validation) const allRoleIds = new Set(); for (const role of ROLES) { allRoleIds.add(role.id); if (role.github) { githubTeamNames.add(role.github.team); } } let hasErrors = false; // Validate team references in REPOSITORY_ACCESS console.log('Validating team references in repoAccess.ts...'); for (const repo of REPOSITORY_ACCESS) { if (!repo.teams) continue; for (const teamRef of repo.teams) { if (!githubTeamNames.has(teamRef.team)) { console.error( `ERROR: Repository "${repo.repository}" references team "${teamRef.team}" which does not exist in roles.ts` ); hasErrors = true; } } } // Validate role references in MEMBERS (memberOf) console.log('Validating role references in users.ts...'); for (const member of MEMBERS) { for (const roleId of member.memberOf) { if (!allRoleIds.has(roleId)) { console.error( `ERROR: Member "${member.github || member.email}" references role "${roleId}" which does not exist in roles.ts` ); hasErrors = true; } } } // Validate member ordering in users.ts console.log('Validating member ordering in users.ts...'); { // Split members into github members and email-only members const githubMembers: (typeof MEMBERS)[number][] = []; const emailOnlyMembers: (typeof MEMBERS)[number][] = []; for (const member of MEMBERS) { if (member.github) { githubMembers.push(member); } else if (member.email) { emailOnlyMembers.push(member); } } // Check that github members come before email-only members let foundEmailOnly = false; for (const member of MEMBERS) { if (!member.github && member.email) { foundEmailOnly = true; } else if (member.github && foundEmailOnly) { console.error( `ERROR: Member "${member.github}" appears after email-only members. GitHub members should come before email-only members.` ); hasErrors = true; break; } } // Check that github members are sorted alphabetically (case-insensitive) for (let i = 1; i < githubMembers.length; i++) { const prev = githubMembers[i - 1].github!.toLowerCase(); const curr = githubMembers[i].github!.toLowerCase(); if (prev > curr) { console.error( `ERROR: Members are not sorted alphabetically. "${githubMembers[i - 1].github}" should come after "${githubMembers[i].github}".` ); hasErrors = true; break; } } } // Validate Google Workspace user provisioning fields console.log('Validating Google Workspace user provisioning fields...'); { const googleEmailPrefixes = new Map(); for (const member of MEMBERS) { const memberId = member.github || member.email || 'unknown'; // Members with googleEmailPrefix must also have firstName and lastName if (member.googleEmailPrefix) { if (!member.firstName) { console.error(`ERROR: Member "${memberId}" has googleEmailPrefix but is missing firstName`); hasErrors = true; } if (!member.lastName) { console.error(`ERROR: Member "${memberId}" has googleEmailPrefix but is missing lastName`); hasErrors = true; } // Check uniqueness of googleEmailPrefix const existing = googleEmailPrefixes.get(member.googleEmailPrefix); if (existing) { console.error( `ERROR: googleEmailPrefix "${member.googleEmailPrefix}" is used by both "${existing}" and "${memberId}"` ); hasErrors = true; } else { googleEmailPrefixes.set(member.googleEmailPrefix, memberId); } } // Members in provisionUser roles without all three fields won't get a GWS account const inProvisionUserRole = hasProvisionUserRole(member.memberOf, roleLookup); const hasProvisioningFields = !!( member.googleEmailPrefix && member.firstName && member.lastName ); // A googleEmailPrefix on a non-eligible member is a silent no-op in // src/google.ts — no account gets created and nothing reports it. // Fail loudly instead (members whose account already exists in GWS are // exempt; those accounts are not managed by Pulumi). if (member.googleEmailPrefix && !inProvisionUserRole && !member.existingGWSUser) { console.error( `ERROR: Member "${memberId}" has googleEmailPrefix but none of their roles ` + `(or parent roles) has provisionUser, so no GWS account would be created. ` + `Add provisionUser: true to an appropriate role in src/config/roles.ts or ` + `remove the provisioning fields.` ); hasErrors = true; } if (member.skipGoogleUserProvisioning && !inProvisionUserRole) { console.error( `ERROR: Member "${memberId}" has skipGoogleUserProvisioning=true but is not in a provisionUser role` ); hasErrors = true; } if (inProvisionUserRole && hasProvisioningFields && member.skipGoogleUserProvisioning) { console.error( `ERROR: Member "${memberId}" has provisioning fields and skipGoogleUserProvisioning=true; pick one` ); hasErrors = true; } } } // Validate that external group members are explicitly permitted. // allowExternalMembers is an explicit per-role opt-in (see GoogleConfig in // roles.ts): if a group's membership resolves to any non-@modelcontextprotocol.io // email but the role does not opt in, `pulumi up` would set the group's // ALLOW_EXTERNAL_MEMBERS setting to false and Google would silently purge the // external members ~1-2 days later (#133 incident). Fail loudly instead. console.log('Validating external group members are explicitly permitted...'); for (const member of MEMBERS) { const memberEmail = resolveGoogleMemberEmail(member); if (!memberEmail || memberEmail.endsWith('@modelcontextprotocol.io')) continue; for (const roleId of member.memberOf) { const role = roleLookup.get(roleId); if (!role?.google || role.google.allowExternalMembers === true) continue; console.error( `ERROR: Google group "${role.google.group}" has external member "${memberEmail}" ` + `but its role does not permit external members. If this member is intentional, ` + `add allowExternalMembers: true to the '${role.id}' role's google config in src/config/roles.ts` ); hasErrors = true; } } // Validate npm/PyPI identities and packageAccess.ts references console.log('Validating package registry references in packageAccess.ts...'); { // npm/pypi usernames must be unique across members const npmUsers = new Map(); const pypiUsers = new Map(); for (const member of MEMBERS) { const memberId = member.github || member.email || 'unknown'; for (const [field, seen] of [ ['npm', npmUsers], ['pypi', pypiUsers], ] as const) { const username = member[field]; if (!username) continue; const existing = seen.get(username); if (existing) { console.error( `ERROR: ${field} username "${username}" is used by both "${existing}" and "${memberId}"` ); hasErrors = true; } else { seen.set(username, memberId); } } } // Unmapped lists must not overlap with mapped members (stale entries) for (const username of UNMAPPED_NPM_USERS) { if (npmUsers.has(username)) { console.error( `ERROR: npm username "${username}" is in UNMAPPED_NPM_USERS but is already mapped ` + `to member "${npmUsers.get(username)}" — remove it from the unmapped list` ); hasErrors = true; } } for (const username of UNMAPPED_PYPI_USERS) { if (pypiUsers.has(username)) { console.error( `ERROR: PyPI username "${username}" is in UNMAPPED_PYPI_USERS but is already mapped ` + `to member "${pypiUsers.get(username)}" — remove it from the unmapped list` ); hasErrors = true; } } // Every npm maintainer referenced by a package must be a known account const knownNpm = new Set([...npmUsers.keys(), ...UNMAPPED_NPM_USERS]); const npmPackageNames = new Set(); for (const pkg of NPM_PACKAGES) { if (npmPackageNames.has(pkg.package)) { console.error(`ERROR: Package "${pkg.package}" is declared twice in packageAccess.ts`); hasErrors = true; } npmPackageNames.add(pkg.package); if (!pkg.package.startsWith('@modelcontextprotocol/')) { console.error(`ERROR: Package "${pkg.package}" is not in the @modelcontextprotocol scope`); hasErrors = true; } for (const username of pkg.maintainers) { if (!knownNpm.has(username)) { console.error( `ERROR: Package "${pkg.package}" references npm user "${username}" which is not ` + `declared on any member in users.ts (npm field) or in UNMAPPED_NPM_USERS` ); hasErrors = true; } } if ( pkg.trustedPublisher && !pkg.trustedPublisher.repository.startsWith('modelcontextprotocol/') ) { console.error( `ERROR: Package "${pkg.package}" declares trusted publisher repository ` + `"${pkg.trustedPublisher.repository}" outside the modelcontextprotocol GitHub org` ); hasErrors = true; } } // Every PyPI account referenced by a project must be a known account const knownPypi = new Set([...pypiUsers.keys(), ...UNMAPPED_PYPI_USERS]); const pypiProjectNames = new Set(); for (const project of PYPI_PROJECTS) { if (pypiProjectNames.has(project.project)) { console.error( `ERROR: PyPI project "${project.project}" is declared twice in packageAccess.ts` ); hasErrors = true; } pypiProjectNames.add(project.project); for (const username of project.accounts) { if (!knownPypi.has(username)) { console.error( `ERROR: PyPI project "${project.project}" references PyPI user "${username}" which is not ` + `declared on any member in users.ts (pypi field) or in UNMAPPED_PYPI_USERS` ); hasErrors = true; } } } } // Validate parent role references in roles.ts console.log('Validating parent role references in roles.ts...'); for (const role of ROLES) { if (!role.github?.parent) continue; const parentRole = roleLookup.get(role.github.parent); if (!parentRole) { console.error( `ERROR: Role "${role.id}" has parent "${role.github.parent}" which does not exist in roles.ts` ); hasErrors = true; } else if (!parentRole.github) { console.error( `ERROR: Role "${role.id}" has parent "${role.github.parent}" which does not have GitHub config` ); hasErrors = true; } } if (hasErrors) { console.error('\nValidation failed! Please fix the errors above.'); process.exit(1); } else { console.log('\nAll references are valid.'); process.exit(0); }