Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ jobs:
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
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ jobs:
with:
pulumi-version: ${{ env.PULUMI_VERSION }}

- 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
Expand All @@ -52,6 +62,10 @@ jobs:
env:
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
GITHUB_TOKEN: ${{ secrets.PULUMI_GITHUB_TOKEN }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }}
run: |
echo "$PULUMI_PASSPHRASE" > passphrase.prod.txt
pulumi config set discord:guildId "$DISCORD_GUILD_ID" --stack prod
pulumi config set discord:botToken "$DISCORD_BOT_TOKEN" --secret --stack prod
make up
153 changes: 153 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Preview

on:
pull_request:
branches:
- main

permissions:
contents: read
pull-requests: write

env:
PULUMI_VERSION: "3.197.0"

jobs:
preview:
name: Preview Changes
runs-on: ubuntu-latest
environment: production
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 }}

- 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: Run validation
run: npm run check

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}

- name: Preview changes
id: preview
env:
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
GITHUB_TOKEN: ${{ secrets.PULUMI_GITHUB_TOKEN }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }}
run: |
echo "$PULUMI_PASSPHRASE" > passphrase.prod.txt
pulumi login gs://mcp-access-prod-pulumi-state

# Build config flags for Discord if secrets are available
CONFIG_FLAGS=""
if [ -n "$DISCORD_GUILD_ID" ]; then
CONFIG_FLAGS="$CONFIG_FLAGS --config discord:guildId=$DISCORD_GUILD_ID"
fi
if [ -n "$DISCORD_BOT_TOKEN" ]; then
CONFIG_FLAGS="$CONFIG_FLAGS --config discord:botToken=$DISCORD_BOT_TOKEN"
fi

# Run preview and capture output
set +e
PREVIEW_OUTPUT=$(PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi preview --stack prod --diff $CONFIG_FLAGS 2>&1)
PREVIEW_EXIT_CODE=$?
set -e

# Save output for comment
echo "exit_code=$PREVIEW_EXIT_CODE" >> $GITHUB_OUTPUT

# Write preview to file (handles multiline)
echo "$PREVIEW_OUTPUT" > preview_output.txt

# Also print to logs
echo "$PREVIEW_OUTPUT"

# Exit with preview exit code
exit $PREVIEW_EXIT_CODE

- name: Comment on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let output = '';
try {
output = fs.readFileSync('preview_output.txt', 'utf8');
} catch (e) {
output = 'Failed to read preview output';
}

// Truncate if too long for GitHub comment
const maxLength = 60000;
if (output.length > maxLength) {
output = output.substring(0, maxLength) + '\n\n... (truncated)';
}

const body = `## Pulumi Preview

<details>
<summary>Click to expand preview output</summary>

\`\`\`
${output}
\`\`\`

</details>
`;

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('## Pulumi Preview')
);

if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"scripts": {
"build": "tsc",
"validate": "npx ts-node scripts/validate-config.ts",
"test": "npx ts-node scripts/test-config.ts",
"check": "npm run validate && npm run test",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
Expand Down
87 changes: 87 additions & 0 deletions scripts/test-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env npx ts-node

/**
* Tests the configuration structure without needing Pulumi credentials.
* Run with: npx ts-node scripts/test-config.ts
*/

import { ROLES, buildRoleLookup, getRolesForPlatform } from '../src/config/roles';
import { ROLE_IDS, isValidRoleId } from '../src/config/roleIds';
import { MEMBERS } from '../src/config/users';

let passed = 0;
let failed = 0;

function test(name: string, fn: () => boolean) {
try {
if (fn()) {
console.log(`✓ ${name}`);
passed++;
} else {
console.log(`✗ ${name}`);
failed++;
}
} catch (e) {
console.log(`✗ ${name}: ${e}`);
failed++;
}
}

console.log('Testing role configuration...\n');

// Test ROLE_IDS
test('ROLE_IDS has entries', () => Object.keys(ROLE_IDS).length > 0);
test('All ROLE_IDS values are valid', () =>
Object.values(ROLE_IDS).every((id) => isValidRoleId(id)));

// Test ROLES
test('ROLES array is not empty', () => ROLES.length > 0);
test('All roles have id and description', () => ROLES.every((r) => r.id && r.description));
test('All role IDs are unique', () => {
const ids = ROLES.map((r) => r.id);
return ids.length === new Set(ids).size;
});

// Test platform configs
const githubRoles = getRolesForPlatform('github');
const discordRoles = getRolesForPlatform('discord');
const googleRoles = getRolesForPlatform('google');

test('Has GitHub roles', () => githubRoles.length > 0);
test('Has Discord roles', () => discordRoles.length > 0);
test('Has Google roles', () => googleRoles.length > 0);

test('GitHub roles have team names', () => githubRoles.every((r) => r.github?.team));
test('Discord roles have role names', () => discordRoles.every((r) => r.discord?.role));
test('Google roles have group names', () => googleRoles.every((r) => r.google?.group));

// Test parent relationships
const roleLookup = buildRoleLookup();
test('All GitHub parent references are valid', () =>
githubRoles.every((r) => {
if (!r.github?.parent) return true;
const parent = roleLookup.get(r.github.parent);
return parent && parent.github;
}));

// Test members
test('MEMBERS array is not empty', () => MEMBERS.length > 0);
test('All members have at least one identifier', () =>
MEMBERS.every((m) => m.github || m.email || m.discord));
test('All member role references are valid', () =>
MEMBERS.every((m) => m.memberOf.every((id) => roleLookup.has(id))));

// Test specific roles exist
test('CORE_MAINTAINERS role exists', () => !!roleLookup.get(ROLE_IDS.CORE_MAINTAINERS));
test('ADMINISTRATORS role exists (Discord-only)', () => {
const role = roleLookup.get(ROLE_IDS.ADMINISTRATORS);
return role !== undefined && role.discord !== undefined && role.github === undefined;
});
test('TYPESCRIPT_SDK_AUTH role exists (GitHub-only)', () => {
const role = roleLookup.get(ROLE_IDS.TYPESCRIPT_SDK_AUTH);
return role !== undefined && role.github !== undefined && role.discord === undefined;
});

// Summary
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
Loading