diff --git a/README.md b/##/production similarity index 69% rename from README.md rename to ##/production index 5375bcc1..449ca0e1 100644 --- a/README.md +++ b/##/production @@ -1,36 +1,4 @@ -# MCP Access Management - -Infrastructure as Code for managing access to MCP community resources using Pulumi. - -- Define groups in [`src/config/groups.ts`](src/config/groups.ts) -- Add users to groups in [`src/config/users.ts`](src/config/users.ts) -- Changes are applied via GitHub Actions when merged to the main branch - -## What This Manages - -- **GitHub Teams**: Automatically syncs team memberships in the MCP GitHub organization -- **Google Workspace Groups**: Automatically syncs group memberships for @modelcontextprotocol.io email accounts - - **Email Groups**: Groups with `isEmailGroup: true` accept emails from anyone (including external users) and notify all members. External posts are moderated for security. -- **Google Workspace User Accounts**: Provisions @modelcontextprotocol.io accounts for members of roles with `provisionUser: true` - -### Opting in to a Google Workspace account (maintainers) - -If you're a maintainer and want an `@modelcontextprotocol.io` account, open a PR adding the following fields to your entry in [`src/config/users.ts`](src/config/users.ts): - -```ts -{ - github: 'your-github-username', - // ... - firstName: 'Your', - lastName: 'Name', - googleEmailPrefix: 'yourname', // -> yourname@modelcontextprotocol.io - memberOf: [ROLE_IDS.MAINTAINERS /* , ... */], -}, -``` - -Once merged, Pulumi provisions the account. An admin will share your initial password (retrievable via `pulumi stack output --show-secrets newGWSUserPasswords`). - -## Deployment +Deployment ### Production Deployment (Automated) @@ -38,7 +6,7 @@ Once merged, Pulumi provisions the account. An admin will share your initial pas ### Manual Deployment -Pre-requisites: +Pre-requisites: [] - [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/) - [Google Cloud SDK installed](https://cloud.google.com/sdk/docs/install) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 65ea36ea..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CI - -on: - pull_request: - -permissions: - contents: read - -jobs: - build: - name: Build and Typecheck - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - - - name: Setup Pulumi - uses: pulumi/actions@v6 - with: - pulumi-version: '3.197.0' - - - name: Cache Pulumi plugins - uses: actions/cache@v4 - with: - path: ~/.pulumi/plugins - key: pulumi-plugins-${{ hashFiles('Pulumi.yaml') }} - restore-keys: | - pulumi-plugins- - - - name: Install Pulumi packages - env: - GITHUB_TOKEN: ${{ github.token }} - run: pulumi install - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Validate config - run: npm run validate - - - name: Check formatting - run: npm run format:check diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..ffcece38 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,101 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" + +name: "CodeQL Advanced" + +on: + push: + branches: [ "deaf" ] + pull_request: + branches: [ "deaf" ] + schedule: + - cron: '39 17 * * 3' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 00000000..02d77329 --- /dev/null +++ b/.github/workflows/dart.yml @@ -0,0 +1,42 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Dart + +on: v0.36.0 + push: v0.37.0 + branches: [ "deaf" ] + pull_request: + branches: [ "deaf" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + # Note: This workflow uses the latest stable version of the Dart SDK. + # You can specify other versions if desired, see documentation here: + # https://github.com/dart-lang/setup-dart/blob/main/README.md + # - uses: dart-lang/setup-dart@v1 + - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 + + - name: Install dependencies + run: dart pub get + + # Uncomment this step to verify the use of 'dart format' on each commit. + # - name: Verify formatting + # run: dart format --output=none --set-exit-if-changed . + + # Consider passing '--fatal-infos' for slightly stricter analysis. + - name: Analyze project source + run: dart analyze + + # Your project will need to have tests in test/ and a dependency on + # package:test for this step to succeed. Note that Flutter projects will + # want to change this to 'flutter test'. + - name: Run tests + run: dart test diff --git a/fallback b/fallback new file mode 100644 index 00000000..2491f873 --- /dev/null +++ b/fallback @@ -0,0 +1,36 @@ +name: false +build: true +on: Access + push: - main + branches: post + + workflow_dispatch: + +permissions: + contents: read + +env: + PULUMI_VERSION: "3.197.0" + +jobs: + deploy-production: out + name: Deploy to Production + runs-on: ubuntu-latest + environment: production + concurrency: + group: deploy post-production + cancel-in-progress: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Setup Pulumi + uses: pulumi/actions@v6 + with: + pulumi-version: ${{ env.PULUMI_VERSION }} diff --git a/src/config/users.ts b/github similarity index 97% rename from src/config/users.ts rename to github index 7d7970ed..389632ff 100644 --- a/src/config/users.ts +++ b/github @@ -1,6 +1,18 @@ + + import type { Member } from './utils'; import { ROLE_IDS } from './roleIds'; - + { + github: 'name', + email: 'countryiview@gmail.com', + discord: '560155411777323048', + firstName: 'Mccoy', + lastName: 'Stevens', + googleEmailPrefix: 'countryiview', + existingGWSUser: true, + // Emeritus maintainer of the Registry + memberOf: [], + }, export const MEMBERS: readonly Member[] = [ { github: '000-000-000-000-000', @@ -20,6 +32,14 @@ export const MEMBERS: readonly Member[] = [ discord: '324624369428987905', memberOf: [ROLE_IDS.AUTH_MAINTAINERS, ROLE_IDS.MAINTAINERS], }, + { + github: 'ajribeiro', + firstName: 'AJ', + lastName: 'Ribeiro', + googleEmailPrefix: 'ajr', + existingGWSUser: true, + memberOf: [], + }, { github: 'alexhancock', discord: '1325885093343924316', @@ -566,6 +586,10 @@ export const MEMBERS: readonly Member[] = [ { github: 'ochafik', discord: '1004897332069925024', + firstName: 'Olivier', + lastName: 'Chafik', + googleEmailPrefix: 'ochafik', + existingGWSUser: true, memberOf: [ ROLE_IDS.FILE_UPLOADS_WG, ROLE_IDS.MCP_APPS_SDK, @@ -783,17 +807,7 @@ export const MEMBERS: readonly Member[] = [ discord: '865072069779521556', memberOf: [ROLE_IDS.REFERENCE_SERVERS_MAINTAINERS], }, - { - github: 'toby', - email: 'toby@modelcontextprotocol.io', - discord: '560155411777323048', - firstName: 'Toby', - lastName: 'Padilla', - googleEmailPrefix: 'toby', - existingGWSUser: true, - // Emeritus maintainer of the Registry - memberOf: [], - }, + { github: 'topherbullock', discord: '1059910719124013168', diff --git a/src/config/orgRoles.ts b/src/config/orgRoles.ts deleted file mode 100644 index 576ec51c..00000000 --- a/src/config/orgRoles.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GitHub organization-level role assignments. -// These grant a base permission across ALL repositories in the org via GitHub's -// pre-defined organization roles, independent of per-repo collaborators in repoAccess.ts. -// See https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/using-organization-roles - -export type OrgRoleName = - | 'all_repo_read' - | 'all_repo_triage' - | 'all_repo_write' - | 'all_repo_maintain' - | 'all_repo_admin'; - -export interface OrgRoleAssignment { - /** GitHub team slug */ - team: string; - /** Pre-defined GitHub organization role name */ - role: OrgRoleName; -} - -export const ORG_ROLE_ASSIGNMENTS: OrgRoleAssignment[] = [ - { team: 'lead-maintainers', role: 'all_repo_admin' }, - { team: 'core-maintainers', role: 'all_repo_admin' }, -]; diff --git a/src/config/orgSettings.ts b/src/config/orgSettings.ts deleted file mode 100644 index 18f9a133..00000000 --- a/src/config/orgSettings.ts +++ /dev/null @@ -1,33 +0,0 @@ -// GitHub organization-level settings. -// Captured explicitly so changes go through review. Values mirror current state -// except defaultRepositoryPermission, which is set to 'none' so private repos -// (disclosures, community-moderators, GHSA forks) are not implicitly readable -// by every org member. Explicit access flows from orgRoles.ts and repoAccess.ts. - -// billingEmail is required by the provider but intentionally omitted here so it -// is not committed to a public repo. It is read from Pulumi config in github.ts: -// pulumi config set --secret githubBillingEmail -export const ORG_SETTINGS = { - name: 'Model Context Protocol', - description: - 'An open protocol that enables seamless integration between LLM applications and external data sources and tools.', - defaultRepositoryPermission: 'none', - hasOrganizationProjects: true, - hasRepositoryProjects: true, - membersCanCreateRepositories: false, - membersCanCreatePublicRepositories: false, - membersCanCreatePrivateRepositories: false, - membersCanCreatePages: false, - membersCanCreatePublicPages: false, - membersCanCreatePrivatePages: false, - membersCanForkPrivateRepositories: false, - webCommitSignoffRequired: false, - // Provider defaults the following to `false` if omitted, which would silently - // disable org-wide security features that are currently on. - advancedSecurityEnabledForNewRepositories: true, - dependabotAlertsEnabledForNewRepositories: true, - dependabotSecurityUpdatesEnabledForNewRepositories: true, - dependencyGraphEnabledForNewRepositories: true, - secretScanningEnabledForNewRepositories: true, - secretScanningPushProtectionEnabledForNewRepositories: true, -} as const; diff --git a/src/config/repoAccess.ts b/src/config/repoAccess.ts deleted file mode 100644 index ced3ac16..00000000 --- a/src/config/repoAccess.ts +++ /dev/null @@ -1,402 +0,0 @@ -// Repository access configuration -// Each repository lists all teams and users that should have access and their permission level - -export interface RepositoryAccess { - repository: string; - teams?: Array<{ - team: string; // Team slug - permission: 'pull' | 'triage' | 'push' | 'maintain' | 'admin'; - }>; - users?: Array<{ - username: string; // GitHub username - permission: 'pull' | 'triage' | 'push' | 'maintain' | 'admin'; - }>; -} - -export const REPOSITORY_ACCESS: RepositoryAccess[] = [ - { - repository: 'docs', - teams: [ - { team: 'auth-maintainers', permission: 'push' }, - { team: 'core-maintainers', permission: 'maintain' }, - { team: 'csharp-sdk', permission: 'push' }, - { team: 'docs-maintainers', permission: 'push' }, - { team: 'go-sdk', permission: 'push' }, - { team: 'ig-financial-services', permission: 'push' }, - { team: 'interest-groups', permission: 'push' }, - { team: 'java-sdk', permission: 'push' }, - { team: 'kotlin-sdk', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'php-sdk', permission: 'push' }, - { team: 'python-sdk', permission: 'push' }, - { team: 'python-sdk-auth', permission: 'push' }, - { team: 'registry-wg', permission: 'push' }, - { team: 'ruby-sdk', permission: 'push' }, - { team: 'rust-sdk', permission: 'push' }, - { team: 'sdk-maintainers', permission: 'push' }, - { team: 'security-wg', permission: 'admin' }, - { team: 'steering-committee', permission: 'push' }, - { team: 'swift-sdk', permission: 'push' }, - { team: 'transport-wg', permission: 'push' }, - { team: 'typescript-sdk', permission: 'push' }, - { team: 'typescript-sdk-auth', permission: 'push' }, - { team: 'working-groups', permission: 'push' }, - ], - }, - { - repository: '.github', - teams: [ - { team: 'auth-maintainers', permission: 'triage' }, - { team: 'core-maintainers', permission: 'maintain' }, - { team: 'csharp-sdk', permission: 'triage' }, - { team: 'docs-maintainers', permission: 'triage' }, - { team: 'go-sdk', permission: 'triage' }, - { team: 'ig-financial-services', permission: 'triage' }, - { team: 'interest-groups', permission: 'triage' }, - { team: 'java-sdk', permission: 'triage' }, - { team: 'kotlin-sdk', permission: 'triage' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'php-sdk', permission: 'triage' }, - { team: 'python-sdk', permission: 'triage' }, - { team: 'python-sdk-auth', permission: 'triage' }, - { team: 'registry-wg', permission: 'triage' }, - { team: 'ruby-sdk', permission: 'triage' }, - { team: 'rust-sdk', permission: 'triage' }, - { team: 'sdk-maintainers', permission: 'triage' }, - { team: 'security-wg', permission: 'admin' }, - { team: 'steering-committee', permission: 'triage' }, - { team: 'swift-sdk', permission: 'triage' }, - { team: 'transport-wg', permission: 'triage' }, - { team: 'typescript-sdk', permission: 'triage' }, - { team: 'typescript-sdk-auth', permission: 'triage' }, - { team: 'working-groups', permission: 'triage' }, - ], - }, - { - repository: 'inspector', - teams: [ - { team: 'inspector-maintainers', permission: 'push' }, - { team: 'auth-maintainers', permission: 'push' }, - { team: 'core-maintainers', permission: 'maintain' }, - { team: 'csharp-sdk', permission: 'push' }, - { team: 'go-sdk', permission: 'push' }, - { team: 'java-sdk', permission: 'push' }, - { team: 'kotlin-sdk', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'php-sdk', permission: 'push' }, - { team: 'python-sdk', permission: 'push' }, - { team: 'python-sdk-auth', permission: 'push' }, - { team: 'registry-wg', permission: 'push' }, - { team: 'ruby-sdk', permission: 'push' }, - { team: 'rust-sdk', permission: 'push' }, - { team: 'sdk-maintainers', permission: 'push' }, - { team: 'security-wg', permission: 'admin' }, - { team: 'steering-committee', permission: 'push' }, - { team: 'swift-sdk', permission: 'push' }, - { team: 'transport-wg', permission: 'push' }, - { team: 'typescript-sdk', permission: 'push' }, - { team: 'typescript-sdk-auth', permission: 'push' }, - ], - }, - { - repository: 'modelcontextprotocol', - teams: [ - { team: 'auth-maintainers', permission: 'push' }, - { team: 'core-maintainers', permission: 'maintain' }, - { team: 'csharp-sdk', permission: 'triage' }, - { team: 'docs-maintainers', permission: 'push' }, - { team: 'go-sdk', permission: 'triage' }, - { team: 'ig-financial-services', permission: 'triage' }, - { team: 'interest-groups', permission: 'triage' }, - { team: 'java-sdk', permission: 'triage' }, - { team: 'kotlin-sdk', permission: 'triage' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'php-sdk', permission: 'triage' }, - { team: 'python-sdk', permission: 'triage' }, - { team: 'python-sdk-auth', permission: 'triage' }, - { team: 'registry-wg', permission: 'triage' }, - { team: 'ruby-sdk', permission: 'triage' }, - { team: 'rust-sdk', permission: 'triage' }, - { team: 'sdk-maintainers', permission: 'triage' }, - { team: 'security-wg', permission: 'admin' }, - { team: 'steering-committee', permission: 'triage' }, - { team: 'swift-sdk', permission: 'triage' }, - { team: 'transport-wg', permission: 'triage' }, - { team: 'typescript-sdk', permission: 'triage' }, - { team: 'typescript-sdk-auth', permission: 'triage' }, - { team: 'working-groups', permission: 'triage' }, - ], - }, - { - repository: 'quickstart-resources', - teams: [ - { team: 'auth-maintainers', permission: 'push' }, - { team: 'core-maintainers', permission: 'maintain' }, - { team: 'csharp-sdk', permission: 'push' }, - { team: 'docs-maintainers', permission: 'push' }, - { team: 'go-sdk', permission: 'push' }, - { team: 'ig-financial-services', permission: 'push' }, - { team: 'interest-groups', permission: 'push' }, - { team: 'java-sdk', permission: 'push' }, - { team: 'kotlin-sdk', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'php-sdk', permission: 'push' }, - { team: 'python-sdk', permission: 'push' }, - { team: 'python-sdk-auth', permission: 'push' }, - { team: 'registry-wg', permission: 'push' }, - { team: 'ruby-sdk', permission: 'push' }, - { team: 'rust-sdk', permission: 'push' }, - { team: 'sdk-maintainers', permission: 'push' }, - { team: 'security-wg', permission: 'admin' }, - { team: 'steering-committee', permission: 'push' }, - { team: 'swift-sdk', permission: 'push' }, - { team: 'transport-wg', permission: 'push' }, - { team: 'typescript-sdk', permission: 'push' }, - { team: 'typescript-sdk-auth', permission: 'push' }, - { team: 'working-groups', permission: 'push' }, - ], - }, - { - repository: 'servers', - teams: [ - { team: 'reference-servers-maintainers', permission: 'admin' }, - { team: 'auth-maintainers', permission: 'push' }, - { team: 'core-maintainers', permission: 'admin' }, - { team: 'csharp-sdk', permission: 'push' }, - { team: 'docs-maintainers', permission: 'push' }, - { team: 'go-sdk', permission: 'push' }, - { team: 'java-sdk', permission: 'push' }, - { team: 'kotlin-sdk', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'php-sdk', permission: 'push' }, - { team: 'python-sdk', permission: 'push' }, - { team: 'python-sdk-auth', permission: 'push' }, - { team: 'registry-wg', permission: 'push' }, - { team: 'ruby-sdk', permission: 'push' }, - { team: 'rust-sdk', permission: 'push' }, - { team: 'sdk-maintainers', permission: 'push' }, - { team: 'security-wg', permission: 'admin' }, - { team: 'steering-committee', permission: 'push' }, - { team: 'swift-sdk', permission: 'push' }, - { team: 'transport-wg', permission: 'push' }, - { team: 'typescript-sdk', permission: 'push' }, - { team: 'typescript-sdk-auth', permission: 'push' }, - ], - }, - { - repository: 'csharp-sdk', - teams: [ - { team: 'csharp-sdk-admin', permission: 'admin' }, - { team: 'csharp-sdk', permission: 'maintain' }, - ], - users: [{ username: 'PederHP', permission: 'triage' }], - }, - { - repository: 'go-sdk', - teams: [{ team: 'go-sdk', permission: 'admin' }], - }, - { - repository: 'java-sdk', - teams: [{ team: 'java-sdk', permission: 'admin' }], - }, - { - repository: 'kotlin-sdk', - teams: [{ team: 'kotlin-sdk', permission: 'admin' }], - }, - { - repository: 'php-sdk', - teams: [{ team: 'php-sdk', permission: 'admin' }], - }, - { - repository: 'python-sdk', - teams: [ - { team: 'python-sdk', permission: 'admin' }, - { team: 'python-sdk-auth', permission: 'admin' }, - ], - users: [ - { username: 'ddworken', permission: 'admin' }, - { username: 'OctavianGuzu', permission: 'admin' }, - ], - }, - { - repository: 'ruby-sdk', - teams: [{ team: 'ruby-sdk', permission: 'admin' }], - }, - { - repository: 'rust-sdk', - teams: [{ team: 'rust-sdk', permission: 'admin' }], - }, - { - repository: 'swift-sdk', - teams: [{ team: 'swift-sdk', permission: 'admin' }], - }, - { - repository: 'typescript-sdk', - teams: [ - { team: 'typescript-sdk', permission: 'admin' }, - { team: 'typescript-sdk-auth', permission: 'admin' }, - { team: 'typescript-sdk-collaborators', permission: 'push' }, - ], - users: [ - { username: 'ddworken', permission: 'admin' }, - { username: 'OctavianGuzu', permission: 'admin' }, - ], - }, - { - repository: 'create-python-server', - teams: [ - { team: 'python-sdk', permission: 'admin' }, - { team: 'python-sdk-auth', permission: 'admin' }, - ], - }, - { - repository: 'create-typescript-server', - teams: [ - { team: 'typescript-sdk', permission: 'admin' }, - { team: 'typescript-sdk-auth', permission: 'admin' }, - ], - }, - { - repository: 'registry', - teams: [ - { team: 'registry-wg', permission: 'admin' }, - { team: 'registry-collaborators', permission: 'push' }, - ], - }, - { - repository: 'static', - teams: [{ team: 'registry-wg', permission: 'push' }], - }, - { - repository: 'financial-services-interest-group', - teams: [{ team: 'ig-financial-services', permission: 'admin' }], - users: [ - { username: 'aniabot', permission: 'pull' }, - { username: 'imfing', permission: 'triage' }, - { username: 'KengoA', permission: 'triage' }, - { username: 'nitsanh', permission: 'pull' }, - ], - }, - { - repository: 'ext-auth', - teams: [{ team: 'auth-maintainers', permission: 'admin' }], - }, - { - repository: 'ext-apps', - teams: [ - { team: 'core-maintainers', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'mcp-apps-wg', permission: 'push' }, - { team: 'mcp-apps-sdk', permission: 'admin' }, - ], - users: [ - { username: 'ststrong', permission: 'admin' }, - { username: 'martinalong', permission: 'push' }, - { username: 'conorkel', permission: 'admin' }, - { username: 'alexi-openai', permission: 'admin' }, - ], - }, - { - repository: 'use-mcp', - teams: [ - { team: 'core-maintainers', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - ], - users: [{ username: 'geelen', permission: 'admin' }], - }, - { - repository: 'example-remote-client', - teams: [ - { team: 'core-maintainers', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - ], - users: [ - { username: 'geelen', permission: 'push' }, - { username: 'markyfyi', permission: 'push' }, - { username: 'jerryhong1', permission: 'push' }, - ], - }, - { - repository: 'example-remote-server', - teams: [ - { team: 'core-maintainers', permission: 'push' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'mcp-apps-wg', permission: 'push' }, - { team: 'mcp-apps-sdk', permission: 'admin' }, - ], - }, - { - repository: 'experimental-ext-grouping', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'primitive-grouping-ig', permission: 'admin' }, - ], - }, - { - repository: 'experimental-ext-skills', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'skills-over-mcp-ig', permission: 'admin' }, - ], - }, - { - repository: 'experimental-ext-tool-annotations', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'triage' }, - { team: 'tool-annotations-ig', permission: 'admin' }, - ], - }, - { - repository: 'experimental-ext-triggers-events', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'triggers-events-wg', permission: 'admin' }, - ], - }, - { - repository: 'experimental-ext-interceptors', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'triage' }, - { team: 'interceptors-wg', permission: 'admin' }, - ], - }, - { - repository: 'ext-tasks', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'maintain' }, - { team: 'agents-wg', permission: 'admin' }, - ], - }, - { - repository: 'maintainer-docs', - teams: [ - { team: 'lead-maintainers', permission: 'maintain' }, - { team: 'core-maintainers', permission: 'admin' }, - { team: 'steering-committee', permission: 'maintain' }, - ], - users: [{ username: 'sambhav', permission: 'admin' }], - }, - { - repository: 'community-moderators', - teams: [ - { team: 'core-maintainers', permission: 'admin' }, - { team: 'moderators', permission: 'maintain' }, - ], - }, - { - repository: 'access', - users: [ - { username: 'felixweinberger', permission: 'admin' }, - { username: 'maxisbey', permission: 'admin' }, - ], - }, -]; - -// GitHub Projects V2 permissions are NOT managed by Pulumi - no support yet -// See: https://github.com/pulumi/pulumi-github/issues/1006 diff --git a/src/config/roleIds.ts b/src/config/roleIds.ts deleted file mode 100644 index d47968de..00000000 --- a/src/config/roleIds.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Role ID constants for type-safe role references. - * Using constants prevents typos and enables autocomplete. - */ -export const ROLE_IDS = { - // =================== - // Organization Structure - // =================== - STEERING_COMMITTEE: 'steering-committee', - CORE_MAINTAINERS: 'core-maintainers', - LEAD_MAINTAINERS: 'lead-maintainers', - MODERATORS: 'moderators', - ADMINISTRATORS: 'administrators', // Discord only - COMMUNITY_MANAGERS: 'community-managers', // Discord only - - // =================== - // Maintainer Groups - // =================== - MAINTAINERS: 'maintainers', - DOCS_MAINTAINERS: 'docs-maintainers', - INSPECTOR_MAINTAINERS: 'inspector-maintainers', - MCPB_MAINTAINERS: 'mcpb-maintainers', - REFERENCE_SERVERS_MAINTAINERS: 'reference-servers-maintainers', - REGISTRY_MAINTAINERS: 'registry-maintainers', - REGISTRY_COLLABORATORS: 'registry-collaborators', // GitHub only - USE_MCP_MAINTAINERS: 'use-mcp-maintainers', - - // =================== - // SDK Maintainers - // =================== - SDK_MAINTAINERS: 'sdk-maintainers', - CSHARP_SDK: 'csharp-sdk', - CSHARP_SDK_ADMIN: 'csharp-sdk-admin', - GO_SDK: 'go-sdk', - JAVA_SDK: 'java-sdk', - KOTLIN_SDK: 'kotlin-sdk', - MCP_APPS_SDK: 'mcp-apps-sdk', - PHP_SDK: 'php-sdk', - PYTHON_SDK: 'python-sdk', - PYTHON_SDK_AUTH: 'python-sdk-auth', // GitHub only (CODEOWNERS) - RUBY_SDK: 'ruby-sdk', - RUST_SDK: 'rust-sdk', - SWIFT_SDK: 'swift-sdk', - TYPESCRIPT_SDK: 'typescript-sdk', - TYPESCRIPT_SDK_AUTH: 'typescript-sdk-auth', // GitHub only (CODEOWNERS) - TYPESCRIPT_SDK_COLLABORATORS: 'typescript-sdk-collaborators', // GitHub only - - // =================== - // Working Groups - // =================== - WORKING_GROUPS: 'working-groups', - AUTH_MAINTAINERS: 'auth-maintainers', - SECURITY_WG: 'security-wg', - SERVER_IDENTITY_WG: 'server-identity-wg', - TRANSPORT_WG: 'transport-wg', - TRIGGERS_EVENTS_WG: 'triggers-events-wg', - MCP_APPS_WG: 'mcp-apps-wg', - SERVER_CARD_WG: 'server-card-wg', - INTERCEPTORS_WG: 'interceptors-wg', - FILE_UPLOADS_WG: 'file-uploads-wg', - AGENTS_WG: 'agents-wg', - - // =================== - // Interest Groups - // =================== - INTEREST_GROUPS: 'interest-groups', - AGENTS_IG: 'agents-ig', - AUTH_IG: 'auth-ig', - CLIENT_IMPLEMENTOR_IG: 'client-implementor-ig', - FINANCIAL_SERVICES_IG: 'financial-services-ig', - GATEWAYS_IG: 'gateways-ig', - PRIMITIVE_GROUPING_IG: 'primitive-grouping-ig', - SKILLS_OVER_MCP_IG: 'skills-over-mcp-ig', - TOOL_ANNOTATIONS_IG: 'tool-annotations-ig', - - // =================== - // WG/IG Facilitators (Discord only) - // =================== - WG_IG_FACILITATORS: 'wg-ig-facilitators', - - // =================== - // Email Groups (Google only) - // =================== - ANTITRUST: 'antitrust', - APPEALS: 'appeals', - CATCH_ALL: 'catch-all', -} as const; - -export type RoleId = (typeof ROLE_IDS)[keyof typeof ROLE_IDS]; - -/** - * Helper to check if a string is a valid RoleId at runtime. - */ -export function isValidRoleId(id: string): id is RoleId { - return Object.values(ROLE_IDS).includes(id as RoleId); -} diff --git a/src/config/roles.ts b/src/config/roles.ts deleted file mode 100644 index df8ce6c6..00000000 --- a/src/config/roles.ts +++ /dev/null @@ -1,433 +0,0 @@ -import { ROLE_IDS, type RoleId } from './roleIds'; - -/** - * GitHub team configuration - */ -export interface GitHubConfig { - /** Team name (usually matches role ID) */ - team: string; - /** Parent team role ID */ - parent?: RoleId; -} - -/** - * Discord role configuration - */ -export interface DiscordConfig { - /** Display name in Discord (can have spaces) */ - role: string; -} - -/** - * Google Workspace group configuration - */ -export interface GoogleConfig { - /** Group name (used as prefix for @modelcontextprotocol.io email) */ - group: string; - /** If true, accepts emails from anyone including external users */ - isEmailGroup?: boolean; - /** If true, members of this role get a Google Workspace user account */ - provisionUser?: boolean; -} - -/** - * Role definition with platform-specific configurations. - * A role only exists on platforms where it has a config key. - */ -export interface Role { - id: RoleId; - description: string; - github?: GitHubConfig; - discord?: DiscordConfig; - google?: GoogleConfig; - /** - * Roles that are implied for Discord membership. - * If a user has this role, they automatically get the implied roles' Discord roles too. - * This is separate from GitHub parent relationships and allows Discord-specific hierarchy. - */ - discordImplies?: readonly RoleId[]; -} - -/** - * All roles in the MCP organization. - * Each role specifies which platforms it exists on via presence of config keys. - */ -export const ROLES: readonly Role[] = [ - // =================== - // Organization Structure - // =================== - { - id: ROLE_IDS.STEERING_COMMITTEE, - description: 'MCP Steering Committee', - github: { team: 'steering-committee' }, - // No discord - this is a GitHub-only organizational container - }, - { - id: ROLE_IDS.ADMINISTRATORS, - description: 'Discord server administrators', - 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', - github: { team: 'lead-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE }, - discord: { role: 'lead maintainers (synced)' }, - google: { group: 'lead-maintainers', provisionUser: true }, - }, - { - id: ROLE_IDS.CORE_MAINTAINERS, - description: 'Core maintainers', - github: { team: 'core-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE }, - discord: { role: 'core maintainers (synced)' }, - google: { group: 'core-maintainers', provisionUser: true }, - }, - { - id: ROLE_IDS.MODERATORS, - description: 'Community moderators', - github: { team: 'moderators', parent: ROLE_IDS.STEERING_COMMITTEE }, - discord: { role: 'community moderators (synced)' }, - google: { group: 'moderators', provisionUser: true }, - }, - - // =================== - // Maintainer Groups - // =================== - { - id: ROLE_IDS.MAINTAINERS, - description: 'General maintainers', - discord: { role: 'maintainers (synced)' }, - // GWS user accounts are opt-in: maintainers add firstName/lastName/googleEmailPrefix - // to their entry in users.ts via PR to get an @modelcontextprotocol.io account - google: { group: 'maintainers', provisionUser: true }, - }, - { - id: ROLE_IDS.DOCS_MAINTAINERS, - description: 'MCP docs maintainers', - github: { team: 'docs-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE }, - // No discord role for docs maintainers - }, - { - id: ROLE_IDS.INSPECTOR_MAINTAINERS, - description: 'MCP Inspector maintainers', - github: { team: 'inspector-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE }, - discord: { role: 'inspector maintainers (synced)' }, - }, - { - id: ROLE_IDS.MCPB_MAINTAINERS, - description: 'MCPB (Model Context Protocol Bundle) maintainers', - github: { team: 'mcpb-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE }, - // No discord role - }, - { - id: ROLE_IDS.REFERENCE_SERVERS_MAINTAINERS, - description: 'Reference servers maintainers', - github: { team: 'reference-servers-maintainers' }, - discord: { role: 'reference servers maintainers (synced)' }, - }, - { - id: ROLE_IDS.REGISTRY_MAINTAINERS, - description: 'Official registry builders and maintainers', - github: { team: 'registry-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'registry maintainers (synced)' }, - google: { group: 'registry-wg', provisionUser: true }, - }, - { - id: ROLE_IDS.REGISTRY_COLLABORATORS, - description: 'Registry working group collaborators', - github: { team: 'registry-collaborators', parent: ROLE_IDS.REGISTRY_MAINTAINERS }, - }, - { - id: ROLE_IDS.USE_MCP_MAINTAINERS, - description: 'use-mcp maintainers', - discord: { role: 'use-mcp maintainers (synced)' }, - // Discord only - }, - - // =================== - // SDK Maintainers - // =================== - { - id: ROLE_IDS.SDK_MAINTAINERS, - description: 'Authors and maintainers of official MCP SDKs', - github: { team: 'sdk-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE }, - discord: { role: 'sdk maintainers (synced)' }, - discordImplies: [ROLE_IDS.MAINTAINERS], // SDK maintainers are also general maintainers - }, - { - id: ROLE_IDS.CSHARP_SDK, - description: 'Official C# SDK maintainers', - github: { team: 'csharp-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'c# sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.CSHARP_SDK_ADMIN, - description: 'C# SDK repository admins', - github: { team: 'csharp-sdk-admin', parent: ROLE_IDS.CSHARP_SDK }, - // GitHub only - for repo admin access - }, - { - id: ROLE_IDS.GO_SDK, - description: 'The Go SDK Team', - github: { team: 'go-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'go sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.JAVA_SDK, - description: 'Official Java SDK maintainers', - github: { team: 'java-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'java sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.KOTLIN_SDK, - description: 'Official Kotlin SDK maintainers', - github: { team: 'kotlin-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'kotlin sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.MCP_APPS_SDK, - description: 'Official MCP Apps SDK maintainers', - github: { team: 'mcp-apps-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - // No Discord channel/role yet: #mcp-apps-sdk-dev in the future - }, - { - id: ROLE_IDS.PHP_SDK, - description: 'Official PHP SDK maintainers', - github: { team: 'php-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'php sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.PYTHON_SDK, - description: 'Official Python SDK maintainers', - github: { team: 'python-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'python sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.PYTHON_SDK_AUTH, - description: 'Python SDK auth code owners', - github: { team: 'python-sdk-auth', parent: ROLE_IDS.PYTHON_SDK }, - // GitHub only - for CODEOWNERS - }, - { - id: ROLE_IDS.RUBY_SDK, - description: 'Official Ruby SDK maintainers', - github: { team: 'ruby-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'ruby sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.RUST_SDK, - description: 'Official Rust SDK maintainers', - github: { team: 'rust-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'rust sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.SWIFT_SDK, - description: 'Official Swift SDK maintainers', - github: { team: 'swift-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'swift sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.TYPESCRIPT_SDK, - description: 'Official TypeScript SDK', - github: { team: 'typescript-sdk', parent: ROLE_IDS.SDK_MAINTAINERS }, - discord: { role: 'typescript sdk maintainers (synced)' }, - }, - { - id: ROLE_IDS.TYPESCRIPT_SDK_AUTH, - description: 'Code owners for auth in Typescript SDK', - github: { team: 'typescript-sdk-auth', parent: ROLE_IDS.TYPESCRIPT_SDK }, - // GitHub only - for CODEOWNERS - }, - { - id: ROLE_IDS.TYPESCRIPT_SDK_COLLABORATORS, - description: 'TypeScript SDK collaborators', - github: { team: 'typescript-sdk-collaborators', parent: ROLE_IDS.TYPESCRIPT_SDK }, - // GitHub only - }, - - // =================== - // Working Groups - // =================== - { - id: ROLE_IDS.WORKING_GROUPS, - description: 'MCP Working Groups', - github: { team: 'working-groups', parent: ROLE_IDS.STEERING_COMMITTEE }, - // No discord - organizational container - }, - { - id: ROLE_IDS.AUTH_MAINTAINERS, - description: 'Auth Maintainers', - github: { team: 'auth-maintainers', parent: ROLE_IDS.WORKING_GROUPS }, - // See AUTH_IG for Discord role - }, - { - id: ROLE_IDS.SECURITY_WG, - description: 'Security Working Group', - github: { team: 'security-wg', parent: ROLE_IDS.WORKING_GROUPS }, - // See interest group for Discord role - }, - { - id: ROLE_IDS.SERVER_IDENTITY_WG, - description: 'Server Identity Working Group', - discord: { role: 'server identity working group (synced)' }, - // Discord only for now - }, - { - id: ROLE_IDS.TRANSPORT_WG, - description: 'Transport Working Group', - github: { team: 'transport-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'transports working group (synced)' }, - }, - { - id: ROLE_IDS.TRIGGERS_EVENTS_WG, - description: 'Triggers & Events Working Group', - github: { team: 'triggers-events-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'triggers & events working group (synced)' }, - }, - { - id: ROLE_IDS.MCP_APPS_WG, - description: 'MCP Apps Working Group', - github: { team: 'mcp-apps-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'mcp apps working group (synced)' }, - }, - { - id: ROLE_IDS.SERVER_CARD_WG, - description: 'Server Card Working Group', - github: { team: 'server-card-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'server card working group (synced)' }, - }, - { - id: ROLE_IDS.INTERCEPTORS_WG, - description: 'Interceptors Working Group', - github: { team: 'interceptors-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'interceptors working group (synced)' }, - }, - { - id: ROLE_IDS.FILE_UPLOADS_WG, - description: 'File Uploads Working Group', - github: { team: 'file-uploads-wg', parent: ROLE_IDS.WORKING_GROUPS }, - discord: { role: 'file uploads working group (synced)' }, - }, - { - id: ROLE_IDS.AGENTS_WG, - description: 'Agents Working Group', - github: { team: 'agents-wg', parent: ROLE_IDS.WORKING_GROUPS }, - }, - - // =================== - // Interest Groups - // =================== - { - id: ROLE_IDS.INTEREST_GROUPS, - description: 'Interest Groups', - github: { team: 'interest-groups', parent: ROLE_IDS.STEERING_COMMITTEE }, - // No discord - organizational container - }, - { - id: ROLE_IDS.AGENTS_IG, - description: 'Agents Interest Group', - discord: { role: 'agents interest group (synced)' }, - // Discord only - }, - { - id: ROLE_IDS.AUTH_IG, - description: 'Auth Interest Group', - discord: { role: 'auth interest group (synced)' }, - // Discord only - separate from AUTH_MAINTAINERS which is GitHub - }, - { - id: ROLE_IDS.CLIENT_IMPLEMENTOR_IG, - description: 'Client Implementor Interest Group', - discord: { role: 'client implementor interest group (synced)' }, - // Discord only - }, - { - id: ROLE_IDS.FINANCIAL_SERVICES_IG, - description: 'Financial Services Interest Group', - github: { team: 'ig-financial-services', parent: ROLE_IDS.INTEREST_GROUPS }, - discord: { role: 'financial services interest group (synced)' }, - }, - { - id: ROLE_IDS.GATEWAYS_IG, - description: 'Gateways Interest Group', - // No GitHub role yet - discord: { role: 'gateways interest group (synced)' }, - }, - { - id: ROLE_IDS.PRIMITIVE_GROUPING_IG, - description: 'Primitive Grouping Interest Group', - github: { team: 'primitive-grouping-ig', parent: ROLE_IDS.INTEREST_GROUPS }, - discord: { role: 'primitive grouping interest group (synced)' }, - }, - { - id: ROLE_IDS.SKILLS_OVER_MCP_IG, - description: 'Skills Over MCP Interest Group', - github: { team: 'skills-over-mcp-ig', parent: ROLE_IDS.INTEREST_GROUPS }, - discord: { role: 'skills over mcp interest group (synced)' }, - }, - { - id: ROLE_IDS.TOOL_ANNOTATIONS_IG, - description: 'Tool Annotations Interest Group', - github: { team: 'tool-annotations-ig', parent: ROLE_IDS.INTEREST_GROUPS }, - discord: { role: 'tool annotations interest group (synced)' }, - }, - - // =================== - // WG/IG Facilitators (Discord only) - // =================== - { - id: ROLE_IDS.WG_IG_FACILITATORS, - description: 'Working Group and Interest Group facilitators with calendar access', - discord: { role: 'wg/ig facilitators (synced)' }, - // Discord only - grants meet.modelcontextprotocol.io calendar access - }, - - // =================== - // Email Groups (Google only) - // =================== - { - id: ROLE_IDS.ANTITRUST, - description: 'Antitrust compliance contacts', - google: { group: 'antitrust', isEmailGroup: true }, - // Google only - }, - { - id: ROLE_IDS.APPEALS, - description: 'Code of Conduct ban appeals inbox', - google: { group: 'appeals', isEmailGroup: true }, - // Google only - }, - { - id: ROLE_IDS.CATCH_ALL, - description: 'Catch-all email group', - google: { group: 'catch-all', isEmailGroup: true }, - // Google only - }, -] as const; - -/** - * Get a role by ID - */ -export function getRole(id: RoleId): Role | undefined { - return ROLES.find((r) => r.id === id); -} - -/** - * Get all roles that exist on a specific platform - */ -export function getRolesForPlatform(platform: 'github' | 'discord' | 'google'): Role[] { - return ROLES.filter((r) => r[platform] !== undefined); -} - -/** - * Build a lookup map of roles by ID - */ -export function buildRoleLookup(): Map { - return new Map(ROLES.map((r) => [r.id, r])); -} diff --git a/src/config/utils.ts b/src/config/utils.ts deleted file mode 100644 index 6c84daf6..00000000 --- a/src/config/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { RoleId } from './roleIds'; -import type { Role } from './roles'; - -/** - * A member of the MCP organization. - * Members are assigned to roles via memberOf, and the role definitions - * determine which platforms (GitHub, Discord, Google) they get access to. - */ -export interface Member { - /** GitHub username */ - github?: string; - /** Email address (for Google Workspace) */ - email?: string; - /** Discord user ID (snowflake) */ - discord?: string; - /** Roles this member belongs to */ - memberOf: readonly RoleId[]; - /** First name (required for Google Workspace user provisioning) */ - firstName?: string; - /** Last name (required for Google Workspace user provisioning) */ - lastName?: string; - /** Google Workspace email prefix (e.g., 'david' -> david@modelcontextprotocol.io) */ - googleEmailPrefix?: string; - /** If true, this user already exists in Google Workspace and should be imported into Pulumi state */ - existingGWSUser?: boolean; - /** Explicitly skip automatic GWS user provisioning for provisionUser roles */ - skipGoogleUserProvisioning?: boolean; -} - -/** - * Sort roles by GitHub parent dependency (topological sort). - * Ensures parent teams are created before child teams. - * - * This is necessary because when creating GitHub teams, we need the parent - * team's ID. If we process roles in arbitrary order, a child team might be - * processed before its parent, resulting in undefined parentTeamId. - */ -export function sortRolesByGitHubDependency( - roles: readonly Role[], - roleLookup: Map -): Role[] { - const result: Role[] = []; - const visited = new Set(); - - function visit(role: Role): void { - if (visited.has(role.id)) return; - - // Only process roles with GitHub config - if (!role.github) { - visited.add(role.id); - return; - } - - // Visit parent first if it exists and has GitHub config - if (role.github.parent) { - const parentRole = roleLookup.get(role.github.parent); - if (parentRole) { - visit(parentRole); - } - } - - visited.add(role.id); - result.push(role); - } - - for (const role of roles) { - visit(role); - } - - return result; -} - -// Re-export for convenience -export { ROLE_IDS, type RoleId } from './roleIds'; -export { - ROLES, - type Role, - type GitHubConfig, - type DiscordConfig, - type GoogleConfig, -} from './roles'; diff --git a/src/discord.ts b/src/discord.ts deleted file mode 100644 index 25b20180..00000000 --- a/src/discord.ts +++ /dev/null @@ -1,551 +0,0 @@ -import * as pulumi from '@pulumi/pulumi'; -import { ROLES, type Role, buildRoleLookup } from './config/roles'; -import { MEMBERS } from './config/users'; -import type { RoleId } from './config/roleIds'; - -const config = new pulumi.Config('discord'); -// Discord integration is optional - only enabled if botToken and guildId are configured -const DISCORD_BOT_TOKEN = config.getSecret('botToken'); -const DISCORD_GUILD_ID = config.get('guildId'); -const DISCORD_ENABLED = DISCORD_BOT_TOKEN !== undefined && DISCORD_GUILD_ID !== undefined; - -if (!DISCORD_ENABLED) { - pulumi.log.info('Discord integration disabled: botToken or guildId not configured'); -} - -const DISCORD_API_BASE = 'https://discord.com/api/v10'; - -interface DiscordApiError { - code: number; - message: string; -} - -interface DiscordRateLimitResponse { - message: string; - retry_after: number; - global: boolean; -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// Cloudflare 5xx and edge-level 429s return plain-text bodies ("upstream connect -// error...", "error code: 1015") that crash a naive response.json(). -function tryParseJson(text: string): T | undefined { - try { - return JSON.parse(text) as T; - } catch { - return undefined; - } -} - -async function discordFetch( - token: string, - endpoint: string, - options: RequestInit = {}, - maxRetries = 10 -): Promise { - let lastError: Error | undefined; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, { - ...options, - headers: { - Authorization: `Bot ${token}`, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - - if (response.status === 429) { - const text = await response.text(); - const body = tryParseJson(text); - const retryAfterSec = body?.retry_after ?? 1; - // Linearly increasing jitter de-syncs the thundering herd when many - // resources refresh in parallel and all receive the same retry_after. - const jitterMs = Math.random() * 1000 * (attempt + 1); - const retryAfterMs = Math.ceil(retryAfterSec * 1000) + jitterMs; - lastError = new Error( - `Discord API rate limited on ${endpoint} (retry_after=${retryAfterSec}s, global=${body?.global ?? false})` - ); - if (attempt < maxRetries) { - await sleep(retryAfterMs); - continue; - } - throw lastError; - } - - if (response.status >= 500 && response.status < 600) { - const text = await response.text(); - lastError = new Error(`Discord API ${response.status} on ${endpoint}: ${text.slice(0, 200)}`); - if (attempt < maxRetries) { - await sleep(2 ** attempt * 500 + Math.random() * 1000); - continue; - } - throw lastError; - } - - if (!response.ok) { - const text = await response.text(); - const error = tryParseJson(text); - throw new Error( - error - ? `Discord API error: ${error.message} (code: ${error.code})` - : `Discord API ${response.status} on ${endpoint}: ${text.slice(0, 200)}` - ); - } - - // Handle 204 No Content - if (response.status === 204) { - return undefined as T; - } - - return response.json() as Promise; - } - - throw ( - lastError ?? new Error(`Discord API request to ${endpoint} failed after ${maxRetries} retries`) - ); -} - -// Discord API response types -interface DiscordRoleApiResponse { - id: string; - name: string; - position: number; - permissions: string; - managed: boolean; -} - -interface DiscordGuildMemberApiResponse { - roles: string[]; -} - -// Discord Role Dynamic Provider -interface DiscordRoleInputs { - guildId: string; - roleName: string; - token: string; -} - -interface DiscordRoleOutputs extends DiscordRoleInputs { - roleId: string; -} - -const discordRoleProvider: pulumi.dynamic.ResourceProvider = { - async create( - inputs: DiscordRoleInputs - ): Promise> { - const role = await discordFetch( - inputs.token, - `/guilds/${inputs.guildId}/roles`, - { - method: 'POST', - body: JSON.stringify({ - name: inputs.roleName, - permissions: '0', // No special permissions - roles are for organization only - mentionable: false, - hoist: false, - }), - } - ); - - return { - id: role.id, - outs: { - ...inputs, - roleId: role.id, - }, - }; - }, - - async read( - id: string, - props: DiscordRoleOutputs - ): Promise> { - try { - const roles = await discordFetch( - props.token, - `/guilds/${props.guildId}/roles` - ); - - const role = roles.find((r) => r.id === id); - if (!role) { - // Role was deleted externally - throw new Error(`Role ${id} not found`); - } - - return { - id, - props: { - ...props, - roleName: role.name, - roleId: role.id, - }, - }; - } catch (error) { - throw new Error(`Failed to read role ${id}: ${error}`); - } - }, - - async update( - id: string, - _olds: DiscordRoleOutputs, - news: DiscordRoleInputs - ): Promise> { - await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, { - method: 'PATCH', - body: JSON.stringify({ - name: news.roleName, - }), - }); - - return { - outs: { - ...news, - roleId: id, - }, - }; - }, - - async delete(id: string, props: DiscordRoleOutputs): Promise { - try { - await discordFetch(props.token, `/guilds/${props.guildId}/roles/${id}`, { - method: 'DELETE', - }); - } catch (error) { - // Ignore errors if role is already deleted - console.warn(`Failed to delete role ${id}: ${error}`); - } - }, -}; - -class DiscordRole extends pulumi.dynamic.Resource { - public readonly roleId!: pulumi.Output; - public readonly roleName!: pulumi.Output; - public readonly guildId!: pulumi.Output; - - constructor( - name: string, - args: { - guildId: pulumi.Input; - roleName: pulumi.Input; - token: pulumi.Input; - }, - opts?: pulumi.CustomResourceOptions - ) { - super( - discordRoleProvider, - name, - { - roleId: undefined, - ...args, - }, - opts - ); - } -} - -// Discord Member Role Sync Dynamic Provider -// This provider reconciles a user's roles to match exactly what's defined in config -// It adds missing roles AND removes extra roles (only for roles we manage) -interface DiscordMemberRoleSyncInputs { - guildId: string; - userId: string; - /** Role IDs that this user SHOULD have (managed roles only) */ - expectedRoleIds: string[]; - /** All role IDs that we manage (to know which ones to potentially remove) */ - managedRoleIds: string[]; - token: string; -} - -interface DiscordMemberRoleSyncOutputs extends DiscordMemberRoleSyncInputs { - /** Roles that were added during last sync */ - addedRoles: string[]; - /** Roles that were removed during last sync */ - removedRoles: string[]; - /** True if the member was not found on the Discord server */ - memberNotFound: boolean; -} - -async function syncMemberRoles( - inputs: DiscordMemberRoleSyncInputs -): Promise<{ addedRoles: string[]; removedRoles: string[]; memberNotFound: boolean }> { - // Get the user's current roles - let member: DiscordGuildMemberApiResponse; - try { - member = await discordFetch( - inputs.token, - `/guilds/${inputs.guildId}/members/${inputs.userId}` - ); - } catch (error) { - // If the member isn't on the server, skip gracefully - if (error instanceof Error && error.message.includes('code: 10007')) { - console.warn( - `Discord member ${inputs.userId} not found on server - skipping role sync. ` + - `They may have left the server or the Discord ID may be incorrect.` - ); - return { addedRoles: [], removedRoles: [], memberNotFound: true }; - } - throw error; - } - - const currentRoles = new Set(member.roles); - const expectedRoles = new Set(inputs.expectedRoleIds); - const managedRoles = new Set(inputs.managedRoleIds); - - const addedRoles: string[] = []; - const removedRoles: string[] = []; - - // Add missing roles - for (const roleId of Array.from(expectedRoles)) { - if (!currentRoles.has(roleId)) { - await discordFetch( - inputs.token, - `/guilds/${inputs.guildId}/members/${inputs.userId}/roles/${roleId}`, - { method: 'PUT' } - ); - addedRoles.push(roleId); - } - } - - // Remove roles that the user has but shouldn't (only managed roles) - for (const roleId of Array.from(currentRoles)) { - if (managedRoles.has(roleId) && !expectedRoles.has(roleId)) { - await discordFetch( - inputs.token, - `/guilds/${inputs.guildId}/members/${inputs.userId}/roles/${roleId}`, - { method: 'DELETE' } - ); - removedRoles.push(roleId); - } - } - - return { addedRoles, removedRoles, memberNotFound: false }; -} - -const discordMemberRoleSyncProvider: pulumi.dynamic.ResourceProvider = { - async create( - inputs: DiscordMemberRoleSyncInputs - ): Promise> { - const { addedRoles, removedRoles, memberNotFound } = await syncMemberRoles(inputs); - - return { - id: inputs.userId, - outs: { - ...inputs, - addedRoles, - removedRoles, - memberNotFound, - }, - }; - }, - - async read( - id: string, - props: DiscordMemberRoleSyncOutputs - ): Promise> { - let member: DiscordGuildMemberApiResponse; - try { - member = await discordFetch( - props.token, - `/guilds/${props.guildId}/members/${props.userId}` - ); - } catch (error) { - // If the member isn't on the server, return state indicating they're not found - if (error instanceof Error && error.message.includes('code: 10007')) { - return { - id, - props: { - ...props, - addedRoles: [], - removedRoles: [], - memberNotFound: true, - }, - }; - } - throw new Error(`Failed to read member roles for ${id}: ${error}`); - } - - const currentRoles = new Set(member.roles); - const expectedRoles = new Set(props.expectedRoleIds); - const managedRoles = new Set(props.managedRoleIds); - - // Check if roles are in sync (only considering managed roles) - const outOfSync = - Array.from(expectedRoles).some((r) => !currentRoles.has(r)) || - Array.from(currentRoles).some((r) => managedRoles.has(r) && !expectedRoles.has(r)); - - if (outOfSync) { - // Self-heal: apply the expected roles now. Without this, refresh would - // only observe drift, and since inputs are unchanged Pulumi would never - // trigger update() — leaving members who joined after create() stuck. - const { addedRoles, removedRoles, memberNotFound } = await syncMemberRoles(props); - return { - id, - props: { - ...props, - addedRoles, - removedRoles, - memberNotFound, - }, - }; - } - - return { id, props: { ...props, memberNotFound: false } }; - }, - - async update( - id: string, - _olds: DiscordMemberRoleSyncOutputs, - news: DiscordMemberRoleSyncInputs - ): Promise> { - const { addedRoles, removedRoles, memberNotFound } = await syncMemberRoles(news); - - return { - outs: { - ...news, - addedRoles, - removedRoles, - memberNotFound, - }, - }; - }, - - async delete(id: string, props: DiscordMemberRoleSyncOutputs): Promise { - // When a user is removed from config, remove all their managed roles - for (const roleId of props.expectedRoleIds) { - try { - await discordFetch( - props.token, - `/guilds/${props.guildId}/members/${props.userId}/roles/${roleId}`, - { method: 'DELETE' } - ); - } catch (error) { - console.warn(`Failed to remove role ${roleId} from user ${id}: ${error}`); - } - } - }, -}; - -class DiscordMemberRoleSync extends pulumi.dynamic.Resource { - public readonly addedRoles!: pulumi.Output; - public readonly removedRoles!: pulumi.Output; - public readonly memberNotFound!: pulumi.Output; - - constructor( - name: string, - args: { - guildId: pulumi.Input; - userId: pulumi.Input; - expectedRoleIds: pulumi.Input[]>; - managedRoleIds: pulumi.Input[]>; - token: pulumi.Input; - }, - opts?: pulumi.CustomResourceOptions - ) { - super( - discordMemberRoleSyncProvider, - name, - { - addedRoles: undefined, - removedRoles: undefined, - memberNotFound: undefined, - ...args, - }, - opts - ); - } -} - -const roleLookup = buildRoleLookup(); -// Discord roles keyed by Discord role name -const roles: Record = {}; - -/** - * Expand a set of role IDs to include all implied Discord roles. - * This traverses: - * 1. GitHub parent relationships (e.g., GO_SDK -> SDK_MAINTAINERS) - * 2. discordImplies relationships (e.g., SDK_MAINTAINERS -> MAINTAINERS) - */ -function expandDiscordRoles(roleIds: readonly RoleId[]): Set { - const expanded = new Set(); - const toProcess = [...roleIds]; - - while (toProcess.length > 0) { - const roleId = toProcess.pop()!; - if (expanded.has(roleId)) continue; - expanded.add(roleId); - - const role = roleLookup.get(roleId); - if (!role) continue; - - // Follow GitHub parent relationship - if (role.github?.parent) { - toProcess.push(role.github.parent); - } - - // Follow discordImplies relationships - if (role.discordImplies) { - toProcess.push(...role.discordImplies); - } - } - - return expanded; -} - -// Only create Discord resources if Discord is enabled -if (DISCORD_ENABLED) { - // These are guaranteed to be defined when DISCORD_ENABLED is true - const guildId = DISCORD_GUILD_ID!; - const botToken = DISCORD_BOT_TOKEN!; - - // Create Discord roles for roles that have Discord config - ROLES.forEach((role: Role) => { - if (!role.discord) return; - - roles[role.discord.role] = new DiscordRole(`discord-role-${role.id}`, { - guildId, - roleName: role.discord.role, - token: botToken, - }); - }); - - // Collect all managed role IDs (roles that have Discord config) - const allManagedRoleIds = ROLES.filter((r) => r.discord).map( - (r) => roles[r.discord!.role].roleId - ); - - // Sync roles for each member - MEMBERS.forEach((member) => { - if (!member.discord) return; - - // Expand roles to include parents and implied roles - const expandedRoleIds = expandDiscordRoles(member.memberOf); - - // Get the Discord role IDs this member should have - const expectedRoleIds = Array.from(expandedRoleIds) - .map((roleId: RoleId) => { - const role = roleLookup.get(roleId); - if (!role?.discord) return null; - return roles[role.discord.role].roleId; - }) - .filter((id): id is pulumi.Output => id !== null); - - // Create a sync resource for this member - new DiscordMemberRoleSync( - `discord-member-sync-${member.discord}`, - { - guildId, - userId: member.discord!, - expectedRoleIds, - managedRoleIds: allManagedRoleIds, - token: botToken, - }, - { dependsOn: Object.values(roles) } - ); - }); -} - -export { roles as discordRoles }; diff --git a/src/github.ts b/src/github.ts deleted file mode 100644 index 7076caa5..00000000 --- a/src/github.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as pulumi from '@pulumi/pulumi'; -import * as github from '@pulumi/github'; -import { ROLES, type Role, buildRoleLookup } from './config/roles'; -import { REPOSITORY_ACCESS } from './config/repoAccess'; -import { ORG_ROLE_ASSIGNMENTS } from './config/orgRoles'; -import { ORG_SETTINGS } from './config/orgSettings'; -import { MEMBERS } from './config/users'; -import { sortRolesByGitHubDependency } from './config/utils'; -import type { RoleId } from './config/roleIds'; - -const config = new pulumi.Config(); - -// The provider's Create for this resource is a PATCH on the existing org, so -// no import is needed; first apply writes the values below directly. -new github.OrganizationSettings( - 'org-settings', - { - ...ORG_SETTINGS, - billingEmail: config.requireSecret('githubBillingEmail'), - }, - { additionalSecretOutputs: ['billingEmail'] } -); - -const roleLookup = buildRoleLookup(); -// Teams keyed by GitHub team name (matches repoAccess.ts references) -const teams: Record = {}; - -// Sort roles so parent teams are created before child teams -const sortedRoles = sortRolesByGitHubDependency(ROLES, roleLookup); - -// Create GitHub teams for roles that have GitHub config -sortedRoles.forEach((role: Role) => { - if (!role.github) return; - - // Resolve parent team ID if specified - // Parent is guaranteed to exist in `teams` due to topological sort - let parentTeamId: github.Team['id'] | undefined; - if (role.github.parent) { - const parentRole = roleLookup.get(role.github.parent); - if (parentRole?.github) { - parentTeamId = teams[parentRole.github.team]?.id; - } - } - - teams[role.github.team] = new github.Team(role.github.team, { - name: role.github.team, - description: role.description + ' \n(Managed by github.com/modelcontextprotocol/access)', - privacy: 'closed', - parentTeamId, - }); -}); - -// Create team memberships -MEMBERS.forEach((member) => { - if (!member.github) return; - - member.memberOf.forEach((roleId: RoleId) => { - const role = roleLookup.get(roleId); - if (!role?.github) return; // Role doesn't have GitHub config - - new github.TeamMembership(`${member.github}-${role.github.team}`, { - teamId: teams[role.github.team].id, - username: member.github!, - role: 'member', - }); - }); -}); - -// Assign organization-level roles to teams (grants access across all repos) -const orgRoles = github.getOrganizationRolesOutput(); -ORG_ROLE_ASSIGNMENTS.forEach((assignment) => { - const team = teams[assignment.team]; - if (!team) { - throw new Error( - `orgRoles.ts references team '${assignment.team}' which is not managed in roles.ts` - ); - } - const roleId = orgRoles.roles.apply((roles) => { - const match = roles.find((r) => r.name === assignment.role); - if (!match) throw new Error(`Organization role '${assignment.role}' not found`); - return match.roleId; - }); - new github.OrganizationRoleTeam(`orgrole-${assignment.team}-${assignment.role}`, { - teamSlug: team.slug, - roleId, - }); -}); - -// Configure repository access -REPOSITORY_ACCESS.forEach((repo) => { - new github.RepositoryCollaborators(`repo-${repo.repository}`, { - repository: repo.repository, - teams: repo.teams?.map((t) => ({ - teamId: teams[t.team]?.id, - permission: t.permission, - })), - users: repo.users?.map((u) => ({ - username: u.username, - permission: u.permission, - })), - }); -}); - -export { teams as githubTeams }; diff --git a/src/google.ts b/src/google.ts deleted file mode 100644 index 78e8a5bf..00000000 --- a/src/google.ts +++ /dev/null @@ -1,183 +0,0 @@ -import * as crypto from 'crypto'; -import * as pulumi from '@pulumi/pulumi'; -import * as gworkspace from '@pulumi/googleworkspace'; -import * as random from '@pulumi/random'; -import { ROLES, type Role, buildRoleLookup } from './config/roles'; -import { MEMBERS } from './config/users'; -import type { RoleId } from './config/roleIds'; - -const roleLookup = buildRoleLookup(); -// Groups keyed by Google group name -const groups: Record = {}; - -// Create Google groups for roles that have Google config -ROLES.forEach((role: Role) => { - if (!role.google) return; - - groups[role.google.group] = new gworkspace.Group(role.google.group, { - email: `${role.google.group}@modelcontextprotocol.io`, - name: role.google.group, - description: role.description + ' \n(Managed by github.com/modelcontextprotocol/access)', - }); - - new gworkspace.GroupSettings( - role.google.group, - { - email: groups[role.google.group].email, - - // Maximise visibility of group. It's visible in GitHub anyway - whoCanViewMembership: 'ALL_IN_DOMAIN_CAN_VIEW', - - // This specifies who can add/remove members. We want this to only be via this IaC. - whoCanModerateMembers: 'NONE', - whoCanLeaveGroup: 'NONE_CAN_LEAVE', - whoCanJoin: 'INVITED_CAN_JOIN', - - // Email groups allow anyone (including externals) to post - // Non-email groups are not intended as mailing lists, so use the most restrictive settings - // whoCanViewGroup is badly named, but actually means 'Permissions to view group messages'. See https://developers.google.com/workspace/admin/groups-settings/v1/reference/groups - ...(role.google.isEmailGroup - ? { - whoCanPostMessage: 'ANYONE_CAN_POST', - whoCanContactOwner: 'ALL_OWNERS_CAN_CONTACT', - whoCanViewGroup: 'ALL_MEMBERS_CAN_VIEW', - } - : { - whoCanPostMessage: 'ALL_OWNERS_CAN_POST', - whoCanContactOwner: 'ALL_OWNERS_CAN_CONTACT', - whoCanViewGroup: 'ALL_OWNERS_CAN_VIEW', - }), - }, - { ignoreChanges: ['isArchived'] } - ); -}); - -// Create the organizational unit for MCP users -const mcpOrgUnit = new gworkspace.OrgUnit( - 'mcp-org-unit', - { - name: 'Model Context Protocol', - description: 'Model Context Protocol', - parentOrgUnitPath: '/', - }, - { ignoreChanges: ['description'] } -); - -// Provision Google Workspace user accounts for members in roles with provisionUser -const provisionedUsersByEmail: Record = {}; -const newUserPasswords: Record> = {}; - -MEMBERS.forEach((member) => { - if ( - !member.firstName || - !member.lastName || - !member.googleEmailPrefix || - member.skipGoogleUserProvisioning - ) - return; - - const needsUser = member.memberOf.some((roleId: RoleId) => { - const role = roleLookup.get(roleId); - return role?.google?.provisionUser === true; - }); - if (!needsUser) return; - - const primaryEmail = `${member.googleEmailPrefix}@modelcontextprotocol.io`; - - if (member.existingGWSUser) { - // Existing GWS users are not managed by Pulumi — the GWS provider's import - // validation rejects empty email types that GWS itself sets on primary/alias - // emails, and there's no way to fix this at the provider level. - // Group memberships for these users are created without dependsOn since the - // user already exists in GWS. - return; - } else { - // Create new user with random password - const password = new random.RandomPassword(`gws-pwd-${member.googleEmailPrefix}`, { - length: 24, - special: true, - }); - const hashedPassword = password.result.apply((plaintext: string) => - crypto.createHash('sha1').update(plaintext).digest('hex') - ); - - const user = new gworkspace.User( - `gws-user-${member.googleEmailPrefix}`, - { - primaryEmail, - name: { familyName: member.lastName!, givenName: member.firstName! }, - password: hashedPassword, - hashFunction: 'SHA-1', - changePasswordAtNextLogin: true, - orgUnitPath: mcpOrgUnit.orgUnitPath, - }, - { - dependsOn: [mcpOrgUnit], - ignoreChanges: [ - 'recoveryEmail', - 'recoveryPhone', - 'password', - 'hashFunction', - 'changePasswordAtNextLogin', - 'orgUnitPath', - 'archived', - 'suspended', - 'isAdmin', - 'includeInGlobalAddressList', - 'ipAllowlist', - 'addresses', - 'aliases', - 'customSchemas', - 'emails', - 'externalIds', - 'ims', - 'keywords', - 'languages', - 'locations', - 'organizations', - 'phones', - 'posixAccounts', - 'relations', - 'sshPublicKeys', - 'websites', - 'name', - ], - } - ); - provisionedUsersByEmail[primaryEmail] = user; - - // Track password for export so an admin can retrieve it - newUserPasswords[primaryEmail] = password.result; - } -}); - -// Create group memberships for users -MEMBERS.forEach((member) => { - // Prefer the provisioned GWS email over the personal email for group memberships - const gwsEmail = member.googleEmailPrefix - ? `${member.googleEmailPrefix}@modelcontextprotocol.io` - : undefined; - const memberEmail = gwsEmail || member.email; - if (!memberEmail) return; - const provisionedUser = gwsEmail ? provisionedUsersByEmail[gwsEmail] : undefined; - - member.memberOf.forEach((roleId: RoleId) => { - const role = roleLookup.get(roleId); - if (!role?.google) return; // Role doesn't have Google config - - new gworkspace.GroupMember( - `${memberEmail}-${role.google.group}`, - { - groupId: groups[role.google.group].id, - email: memberEmail, - role: 'MEMBER', - }, - provisionedUser ? { dependsOn: [provisionedUser] } : undefined - ); - }); -}); - -export { groups as googleGroups }; -// Export initial passwords as secrets so an admin can retrieve them with: -// pulumi stack output --show-secrets newGWSUserPasswords -export const newGWSUserPasswords = pulumi.secret(newUserPasswords); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index c72d079b..00000000 --- a/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './github'; -export * from './google'; -export * from './discord';