Skip to content

Commit ce16330

Browse files
committed
Run previews in a separate reviewer-gated environment
Point preview.yml at a new `preview` environment and document that all deploy credentials live as environment secrets in `production` (main-only) and `preview` (required reviewers), with nothing at repository level. Repository secrets resolve for any workflow on any branch, so write access alone was enough to read the org-owner token by pushing a workflow file.
1 parent 0ae1cfd commit ce16330

3 files changed

Lines changed: 31 additions & 6 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
deploy-production:
1717
name: Deploy to Production
1818
runs-on: ubuntu-latest
19+
# Deploy credentials are secrets of this environment (deployment branches:
20+
# main only), not repository secrets. See README.md "Required GitHub
21+
# Secrets (for CI/CD)".
1922
environment: production
2023
concurrency:
2124
group: deploy-production

‎.github/workflows/preview.yml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ jobs:
1818
runs-on: ubuntu-latest
1919
# Skip preview for fork PRs and Dependabot - they don't have access to Actions secrets
2020
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
21-
environment: production
21+
# Deploy credentials are environment secrets, never repository secrets, so a
22+
# workflow pushed to a branch cannot read them. `preview` holds the same
23+
# secrets as `production` but is gated by required reviewers instead of a
24+
# main-only branch policy (previews run on PR branches). See README.md
25+
# "Required GitHub Secrets (for CI/CD)".
26+
environment: preview
2227
steps:
2328
- name: Checkout code
2429
uses: actions/checkout@v4

‎README.md‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ The PR's `pulumi preview` comment shows the repository create. Once merged, the
7373
### One-time setup
7474

7575
1. In the Cloudflare dashboard for the **MCP Domain Account**, create a token dedicated to Access policy role management. Give it a descriptive name so it does not read as a generic API token, e.g. `mcp-access: Access policy role management`, and scope it to only **Account → Access: Apps and Policies → Edit** on the MCP Domain Account. Do not reuse this token for anything else.
76-
2. Add it as the GitHub Actions secret `CLOUDFLARE_ROLE_MANAGEMENT_TOKEN` in the `production` environment (repository settings → Environments → production). The deploy workflow passes it to Pulumi as `cloudflare:roleManagementToken`. Until the secret exists, the Cloudflare module logs "Cloudflare integration disabled: roleManagementToken not configured" and creates nothing, so previews stay green.
76+
2. Add it as the GitHub Actions secret `CLOUDFLARE_ROLE_MANAGEMENT_TOKEN` in both the `production` and `preview` environments (repository settings → Environments; see [Required GitHub Secrets](#required-github-secrets-for-cicd)). The deploy workflow passes it to Pulumi as `cloudflare:roleManagementToken`. Until the secret exists, the Cloudflare module logs "Cloudflare integration disabled: roleManagementToken not configured" and creates nothing, so previews stay green.
7777
3. The account ID and GitHub identity-provider ID are non-secret and live in [`Pulumi.prod.yaml`](Pulumi.prod.yaml).
7878
4. **Adopting the existing policy.** Pulumi's `import` resource option only succeeds when the program's inputs match the live resource, so adoption is two deploys:
7979
- With `cloudflare:importExistingPolicies: "true"` in `Pulumi.prod.yaml`, the first deploy imports the existing `Maintainers` policy (by its `cloudflarePolicyId`) as-is, ignoring its rule lists.
@@ -131,7 +131,14 @@ Pre-requisites:
131131

132132
### Required GitHub Secrets (for CI/CD)
133133

134-
The following secrets must be configured in GitHub Actions for automated deployments:
134+
Deploy credentials are **environment secrets**, not repository secrets. A repository-level Actions secret resolves for any workflow on any branch, so anyone with write access to this repository could read one by pushing a workflow file. Environment secrets resolve only for jobs that declare the environment, and each environment controls which refs may use it:
135+
136+
| Environment | Used by | Protection |
137+
| ------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
138+
| `production` | [`deploy.yml`](.github/workflows/deploy.yml) on `main` | Deployment branches: `main` only. No reviewers, so merges deploy without a manual step. |
139+
| `preview` | [`preview.yml`](.github/workflows/preview.yml) on PRs | Any branch. Required reviewers: `core-maintainers` (self-review allowed), so each preview run waits for one approval under **Review deployments**. |
140+
141+
Both environments hold the same set of secrets. Keep no copy at repository level (repository settings → Secrets and variables → Actions should list only `NPM_READ_TOKEN`, a read-only npm token used by the package drift check).
135142

136143
- **`GCP_PROD_SERVICE_ACCOUNT_KEY`**: GCP service account key
137144
- Used to authenticate with Google Cloud Storage for Pulumi state (`gs://mcp-access-prod-pulumi-state`)
@@ -142,9 +149,17 @@ The following secrets must be configured in GitHub Actions for automated deploym
142149
- Used to decrypt encrypted values in Pulumi stack configuration
143150
- Keep this secure - if lost, you cannot decrypt your Pulumi state
144151

145-
- **`CLOUDFLARE_ROLE_MANAGEMENT_TOKEN`** (optional, `production` environment): Cloudflare token dedicated to Access policy role management, scoped only to **Account → Access: Apps and Policies → Edit** on the MCP Domain Account (not a general-purpose API token)
152+
- **`PULUMI_GITHUB_TOKEN`**: GitHub token with organization owner rights, exported as `GITHUB_TOKEN` for the Pulumi GitHub provider (teams, memberships, repositories, org settings). The most sensitive credential here.
153+
154+
- **`DISCORD_BOT_TOKEN`** and **`DISCORD_GUILD_ID`**: Discord bot credentials for role sync ([`src/discord.ts`](src/discord.ts)). Optional; the Discord module is skipped when either is unset.
155+
156+
- **`ORG_BILLING_EMAIL`**: billing contact applied to the GitHub organization settings (`githubBillingEmail`, required by `src/github.ts`).
157+
158+
- **`CLOUDFLARE_ROLE_MANAGEMENT_TOKEN`** (optional): Cloudflare token dedicated to Access policy role management, scoped only to **Account → Access: Apps and Policies → Edit** on the MCP Domain Account (not a general-purpose API token)
146159
- Used to manage the Cloudflare Access policy for `securityroom.modelcontextprotocol.io` (see [Cloudflare Access (security-room)](#cloudflare-access-security-room))
147160

161+
Rotating a secret means updating it in both environments.
162+
148163
## Initial Setup
149164

150165
If setting up this infrastructure for the first time:
@@ -201,9 +216,11 @@ pulumi config set --secret googleworkspace:credentials "$(cat sa-key.json)"
201216
pulumi config set --secret github:token "ghp_your_github_token_here"
202217
```
203218

204-
### 3. Configure GitHub Actions Secrets
219+
### 3. Configure GitHub Actions Environments and Secrets
205220

206-
Add the CI/CD secrets to GitHub Actions (repository settings → Secrets and variables → Actions):
221+
Create the `production` and `preview` environments as described in [Required GitHub Secrets](#required-github-secrets-for-cicd) (repository settings → Environments), then add the secrets to **both**:
207222

208223
- `GCP_PROD_SERVICE_ACCOUNT_KEY`: Content of `sa-key.json`
209224
- `PULUMI_PROD_PASSPHRASE`: The passphrase you set above
225+
- `PULUMI_GITHUB_TOKEN`: A GitHub token with organization owner rights
226+
- `ORG_BILLING_EMAIL`: The organization billing contact

0 commit comments

Comments
 (0)