Manage GitHub org settings and org-role assignments via Pulumi (#107)
* Manage GitHub org settings and org-role assignments via Pulumi Brings two click-ops org-level configs under IaC and tightens defaults: - OrganizationSettings: default_repository_permission read -> none so private repos are not implicitly readable by every member; repo creation restricted to admins; billingEmail from Pulumi secret; security new-repo defaults pinned so the provider does not reset them. - Org-role assignments: codify lead/core-maintainers -> all_repo_admin. moderators -> all_repo_triage and anthropic -> all_repo_write are intentionally omitted (being removed). * Fix config namespace: use project namespace for githubBillingEmail The github: namespace is reserved for the @pulumi/github provider's own config keys, so github:billingEmail was rejected as an unknown provider option. Use the project default namespace instead. preview.yml uses the same --config flag pattern as the existing Discord secrets.
This commit is contained in:
parent
945668a063
commit
65b5a43451
5 changed files with 98 additions and 0 deletions
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
|
@ -63,6 +63,7 @@ jobs:
|
|||
env:
|
||||
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
|
||||
GITHUB_TOKEN: ${{ secrets.PULUMI_GITHUB_TOKEN }}
|
||||
ORG_BILLING_EMAIL: ${{ secrets.ORG_BILLING_EMAIL }}
|
||||
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
|
||||
DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }}
|
||||
run: |
|
||||
|
|
@ -71,4 +72,5 @@ jobs:
|
|||
pulumi login gs://mcp-access-prod-pulumi-state
|
||||
pulumi config set discord:guildId "$DISCORD_GUILD_ID" --stack prod
|
||||
pulumi config set discord:botToken "$DISCORD_BOT_TOKEN" --secret --stack prod
|
||||
pulumi config set githubBillingEmail "$ORG_BILLING_EMAIL" --secret --stack prod
|
||||
make up
|
||||
|
|
|
|||
4
.github/workflows/preview.yml
vendored
4
.github/workflows/preview.yml
vendored
|
|
@ -63,6 +63,7 @@ jobs:
|
|||
env:
|
||||
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
|
||||
GITHUB_TOKEN: ${{ secrets.PULUMI_GITHUB_TOKEN }}
|
||||
ORG_BILLING_EMAIL: ${{ secrets.ORG_BILLING_EMAIL }}
|
||||
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
|
||||
DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }}
|
||||
run: |
|
||||
|
|
@ -77,6 +78,9 @@ jobs:
|
|||
if [ -n "$DISCORD_BOT_TOKEN" ]; then
|
||||
CONFIG_FLAGS="$CONFIG_FLAGS --config discord:botToken=$DISCORD_BOT_TOKEN"
|
||||
fi
|
||||
if [ -n "$ORG_BILLING_EMAIL" ]; then
|
||||
CONFIG_FLAGS="$CONFIG_FLAGS --config githubBillingEmail=$ORG_BILLING_EMAIL"
|
||||
fi
|
||||
|
||||
# Run preview and capture output
|
||||
set +e
|
||||
|
|
|
|||
23
src/config/orgRoles.ts
Normal file
23
src/config/orgRoles.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// 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' },
|
||||
];
|
||||
33
src/config/orgSettings.ts
Normal file
33
src/config/orgSettings.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// 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 <email>
|
||||
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;
|
||||
|
|
@ -1,10 +1,26 @@
|
|||
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<string, github.Team> = {};
|
||||
|
|
@ -50,6 +66,26 @@ MEMBERS.forEach((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}`, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue