Skip to content
Open
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
58 changes: 34 additions & 24 deletions .github/workflows/discord_bot_pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ jobs:

- name: Collect GitHub Data for Multiple Organizations
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_APP_ID: ${{ secrets.GH_APP_ID }}
GITHUB_APP_PRIVATE_KEY_B64: ${{ secrets.GH_APP_PRIVATE_KEY_B64 }}
PYTHONUNBUFFERED: 1
PYTHONPATH: ${{ github.workspace }}/discord_bot:${{ github.workspace }}
run: |
Expand Down Expand Up @@ -93,6 +94,7 @@ jobs:

sys.path.insert(0, 'src')
from services.github_service import GitHubService
from services.github_app_service import GitHubAppService

print('Getting registered organizations...')
mt_client = get_mt_client()
Expand All @@ -109,24 +111,30 @@ jobs:
print(f' Server ID: {server_id}')
print(f' Data: {server_data}')

# Extract unique GitHub organizations
github_orgs = set()
# Extract unique GitHub installations (preferred) with a stable org key
installations = {}
for server_id, server_config in servers.items():
installation_id = server_config.get('github_installation_id')
github_org = server_config.get('github_org')
if github_org:
github_orgs.add(github_org)
print(f'Found GitHub org: {github_org} from server {server_id}')
if installation_id and github_org:
installations[int(installation_id)] = github_org
print(f'Found installation: {installation_id} for {github_org} (server {server_id})')
else:
print(f'No github_org found in server {server_id}')
print(f'Skipping server {server_id}: missing github_installation_id or github_org')
print(f'Available keys: {list(server_config.keys())}')

print(f'Found {len(installations)} unique installations: {installations}')

print(f'Found {len(github_orgs)} unique organizations: {github_orgs}')

# Collect data for each organization
# Collect data for each installation (GitHub App token)
all_org_data = {}
for github_org in github_orgs:
print(f'Collecting data for organization: {github_org}')
github_service = GitHubService(github_org)
gh_app = GitHubAppService()
for installation_id, github_org in installations.items():
print(f'Collecting data for installation {installation_id} ({github_org})')
token = gh_app.get_installation_access_token(installation_id)
if not token:
print(f'Failed to get installation token for {installation_id}, skipping')
continue
github_service = GitHubService(github_org, token=token, installation_id=installation_id)
raw_data = github_service.collect_organization_data()
all_org_data[github_org] = raw_data
print(f'Collected data for {len(raw_data.get(\"repositories\", {}))} repositories in {github_org}')
Expand Down Expand Up @@ -244,20 +252,22 @@ jobs:

print(f'Stored labels for {labels_stored} repositories in {github_org}')

# Update user contribution data
user_mappings = {}
for doc in mt_client.db.collection('discord_users').stream():
user_mappings[doc.id] = doc.to_dict()
# Update org-scoped user contribution data (per Discord server/org)
user_mappings = {doc.id: doc.to_dict() for doc in mt_client.db.collection('discord_users').stream()}
stored_count = 0

for username, user_data in contributions.items():
# Find Discord users with this GitHub username
for discord_id, user_mapping in user_mappings.items():
if user_mapping.get('github_id') == username:
if mt_client.set_user_mapping(discord_id, {**user_mapping, **user_data}):
stored_count += 1
for discord_id, user_mapping in user_mappings.items():
github_id = user_mapping.get('github_id')
if not github_id:
continue
user_data = contributions.get(github_id)
if not user_data:
continue
org_user_data = {**user_mapping, **user_data}
if mt_client.set_org_document(github_org, 'discord_users', discord_id, org_user_data):
stored_count += 1

print(f'Updated contribution data for {stored_count} users in {github_org}')
print(f'Updated org-scoped contribution data for {stored_count} users in {github_org}')

print('All organization data stored successfully!')
"
Expand Down
68 changes: 67 additions & 1 deletion discord_bot/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Discord Bot Setup Guide

# Quick Start (Hosted Bot Users)

Use this section if you only want to invite the hosted bot and use it in your Discord server.

1. **Invite the bot** using the link provided by the maintainers.
2. In your Discord server, run: `/setup`
3. Click **Install GitHub App** and select the org/repo(s) to track.
4. Each user links their GitHub account with: `/link`
5. (Optional) Configure role rules:
```
/configure roles action:add metric:commits threshold:1 role:@Contributor
/configure roles action:add metric:prs threshold:10 role:@ActiveContributor
/configure roles action:add metric:prs threshold:50 role:@CoreTeam
```

That’s it. No local setup, no tokens, no config files.

# 1. Prerequisites

### Python 3.13 Setup
Expand Down Expand Up @@ -75,6 +92,15 @@ python -m pip install --upgrade pip
pip install -r discord_bot/requirements.txt
```

### Install `fzf` (interactive selector)

The deployment helper uses [`fzf`](https://github.com/junegunn/fzf) for project/region menus. Install it before running any deployment scripts (and ensure `fzf` is on your `PATH`):

- **macOS:** `brew install fzf`
- **Windows:** `choco install fzf` (Chocolatey) or `winget install fzf`
- **Ubuntu/Debian:** `sudo apt install fzf`
- **Fedora:** `sudo dnf install fzf`

# 2. Project Structure

```
Expand Down Expand Up @@ -108,6 +134,9 @@ cp discord_bot/config/.env.example discord_bot/config/.env
- `GITHUB_TOKEN=` (GitHub API access)
- `GITHUB_CLIENT_ID=` (GitHub OAuth app ID)
- `GITHUB_CLIENT_SECRET=` (GitHub OAuth app secret)
- `GITHUB_APP_ID=` (GitHub App ID)
- `GITHUB_APP_PRIVATE_KEY_B64=` (GitHub App private key, base64)
- `GITHUB_APP_SLUG=` (GitHub App slug)
- `REPO_OWNER=` (Your GitHub organization name)
- `OAUTH_BASE_URL=` (Your Cloud Run URL - set in Step 4)

Expand All @@ -121,6 +150,8 @@ Go to your GitHub repository → Settings → Secrets and variables → Actions
- `GOOGLE_CREDENTIALS_JSON`
- `REPO_OWNER`
- `CLOUD_RUN_URL`
- `GH_APP_ID`
- `GH_APP_PRIVATE_KEY_B64`

If you plan to run GitHub Actions from branches other than `main`, also add the matching development secrets so the workflows can deploy correctly:
- `DEV_GOOGLE_CREDENTIALS_JSON`
Expand Down Expand Up @@ -251,7 +282,7 @@ If you plan to run GitHub Actions from branches other than `main`, also add the

**What this does:** Creates a placeholder Cloud Run service to get your stable URL, which you'll need for GitHub OAuth setup.

1. **Run the URL getter script:**
1. **Run the URL getter script** (requires `fzf`, see prerequisites):
```bash
./discord_bot/deployment/get_url.sh
```
Expand Down Expand Up @@ -300,12 +331,47 @@ If you plan to run GitHub Actions from branches other than `main`, also add the
**Example URLs:** If your Cloud Run URL is `https://discord-bot-abcd1234-uc.a.run.app`, then:
- Homepage URL: `https://discord-bot-abcd1234-uc.a.run.app`
- Callback URL: `https://discord-bot-abcd1234-uc.a.run.app/login/github/authorized`
- If you are using the newer hosted flow, set the callback to `YOUR_CLOUD_RUN_URL/auth/callback` instead.

4. **Get Credentials:**
- Click "Register application"
- Copy the "Client ID" → **Add to `.env`:** `GITHUB_CLIENT_ID=your_client_id`
- Click "Generate a new client secret" → Copy it → **Add to `.env`:** `GITHUB_CLIENT_SECRET=your_secret`

### Step 5b: Create GitHub App (GITHUB_APP_ID / PRIVATE_KEY / SLUG)

**What this configures:**
- `.env` file: `GITHUB_APP_ID=...`, `GITHUB_APP_PRIVATE_KEY_B64=...`, `GITHUB_APP_SLUG=...`
- GitHub Secrets: `GH_APP_ID`, `GH_APP_PRIVATE_KEY_B64`

**What this does:** Allows DisgitBot to read repository data without user PATs.

**Where these values come from:**
- `GITHUB_APP_ID`: shown on the GitHub App settings page (App ID field).
- `GITHUB_APP_PRIVATE_KEY_B64`: base64 of the downloaded `.pem` private key.
- `GITHUB_APP_SLUG`: the URL slug of your GitHub App (shown in the app page URL).

1. **Create the GitHub App (org or personal):**
- For org: `https://github.com/organizations/<ORG>/settings/apps`
- For personal: `https://github.com/settings/apps`
2. **Set these URLs:**
- **Homepage URL:** `YOUR_CLOUD_RUN_URL`
- **Setup URL:** `YOUR_CLOUD_RUN_URL/github/app/setup`
- **Callback URL:** leave empty
3. **Permissions (read-only):**
- Metadata (required), Contents, Issues, Pull requests
- Webhooks: OFF
4. **Install target:** choose **Any account** so anyone can install it.
5. **Generate a private key:**
- Download the `.pem` file
- Base64 it (keep BEGIN/END lines): `base64 -w 0 path/to/private-key.pem`
6. **Set `.env` values:**
- `GITHUB_APP_ID=...` (App ID from the GitHub App page)
- `GITHUB_APP_PRIVATE_KEY_B64=...` (base64 from step 5)
- `GITHUB_APP_SLUG=...` (the app slug shown in the app page URL)

**Security note:** Never commit the private key or base64 value to git. Treat it like a password.

### Step 6: Get REPO_OWNER (.env) + REPO_OWNER (GitHub Secret)

**What this configures:**
Expand Down
5 changes: 4 additions & 1 deletion discord_bot/config/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
REPO_OWNER=
OAUTH_BASE_URL=
DISCORD_BOT_CLIENT_ID=
DISCORD_BOT_CLIENT_ID=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY_B64=
GITHUB_APP_SLUG=
82 changes: 70 additions & 12 deletions discord_bot/deployment/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

FZF_AVAILABLE=0
if command -v fzf &>/dev/null; then
FZF_AVAILABLE=1
fi

# Helper functions
print_header() {
echo -e "\n${PURPLE}================================${NC}"
Expand Down Expand Up @@ -43,6 +48,12 @@ ENV_PATH="$ROOT_DIR/config/.env"

print_header

if [ "$FZF_AVAILABLE" -eq 1 ]; then
print_success "fzf detected: you can type to filter options in selection menus."
else
print_warning "fzf not detected; falling back to arrow-key menu navigation."
fi

# Check if gcloud is installed and authenticated
print_step "Checking Google Cloud CLI..."
if ! command -v gcloud &> /dev/null; then
Expand Down Expand Up @@ -132,6 +143,31 @@ interactive_select() {
done
}

fuzzy_select_or_fallback() {
local prompt="$1"
shift
local options=("$@")

if [ "$FZF_AVAILABLE" -eq 1 ]; then
local selection
selection=$(printf '%s\n' "${options[@]}" | fzf --prompt="$prompt> " --height=15 --border --exit-0)
if [ -z "$selection" ]; then
print_warning "Selection cancelled."
exit 0
fi
for i in "${!options[@]}"; do
if [[ "${options[$i]}" == "$selection" ]]; then
INTERACTIVE_SELECTION=$i
return
fi
done
print_error "Unable to match selection."
exit 1
else
interactive_select "$prompt" "${options[@]}"
fi
}

# Function to select Google Cloud Project
select_project() {
print_step "Fetching your Google Cloud projects..."
Expand All @@ -156,7 +192,7 @@ select_project() {
done <<< "$projects"

# Interactive selection
interactive_select "Select a Google Cloud Project:" "${project_options[@]}"
fuzzy_select_or_fallback "Select a Google Cloud Project" "${project_options[@]}"
selection=$INTERACTIVE_SELECTION

PROJECT_ID="${project_ids[$selection]}"
Expand Down Expand Up @@ -297,14 +333,8 @@ create_new_env_file() {
print_warning "Discord Bot Token is required!"
done

# GitHub Token
while true; do
read -p "GitHub Token: " github_token
if [ -n "$github_token" ]; then
break
fi
print_warning "GitHub Token is required!"
done
# GitHub Token (optional for GitHub App mode)
read -p "GitHub Token (optional): " github_token

# GitHub Client ID
read -p "GitHub Client ID: " github_client_id
Expand All @@ -317,6 +347,14 @@ create_new_env_file() {

# OAuth Base URL (optional - will auto-detect on Cloud Run)
read -p "OAuth Base URL (optional): " oauth_base_url

# Discord Bot Client ID
read -p "Discord Bot Client ID: " discord_bot_client_id

# GitHub App configuration (invite-only mode)
read -p "GitHub App ID: " github_app_id
read -p "GitHub App Private Key (base64): " github_app_private_key_b64
read -p "GitHub App Slug: " github_app_slug

# Create .env file
cat > "$ENV_PATH" << EOF
Expand All @@ -326,6 +364,10 @@ GITHUB_CLIENT_ID=$github_client_id
GITHUB_CLIENT_SECRET=$github_client_secret
REPO_OWNER=$repo_owner
OAUTH_BASE_URL=$oauth_base_url
DISCORD_BOT_CLIENT_ID=$discord_bot_client_id
GITHUB_APP_ID=$github_app_id
GITHUB_APP_PRIVATE_KEY_B64=$github_app_private_key_b64
GITHUB_APP_SLUG=$github_app_slug
EOF

print_success ".env file created successfully!"
Expand Down Expand Up @@ -355,6 +397,18 @@ edit_env_file() {

read -p "OAuth Base URL [$OAUTH_BASE_URL]: " new_oauth_base_url
oauth_base_url=${new_oauth_base_url:-$OAUTH_BASE_URL}

read -p "Discord Bot Client ID [$DISCORD_BOT_CLIENT_ID]: " new_discord_bot_client_id
discord_bot_client_id=${new_discord_bot_client_id:-$DISCORD_BOT_CLIENT_ID}

read -p "GitHub App ID [$GITHUB_APP_ID]: " new_github_app_id
github_app_id=${new_github_app_id:-$GITHUB_APP_ID}

read -p "GitHub App Private Key (base64) [$GITHUB_APP_PRIVATE_KEY_B64]: " new_github_app_private_key_b64
github_app_private_key_b64=${new_github_app_private_key_b64:-$GITHUB_APP_PRIVATE_KEY_B64}

read -p "GitHub App Slug [$GITHUB_APP_SLUG]: " new_github_app_slug
github_app_slug=${new_github_app_slug:-$GITHUB_APP_SLUG}

# Update .env file
cat > "$ENV_PATH" << EOF
Expand All @@ -364,6 +418,10 @@ GITHUB_CLIENT_ID=$github_client_id
GITHUB_CLIENT_SECRET=$github_client_secret
REPO_OWNER=$repo_owner
OAUTH_BASE_URL=$oauth_base_url
DISCORD_BOT_CLIENT_ID=$discord_bot_client_id
GITHUB_APP_ID=$github_app_id
GITHUB_APP_PRIVATE_KEY_B64=$github_app_private_key_b64
GITHUB_APP_SLUG=$github_app_slug
EOF

print_success ".env file updated successfully!"
Expand Down Expand Up @@ -469,7 +527,7 @@ get_deployment_config() {
"custom"
)

interactive_select "Select a Google Cloud Region:" "${region_options[@]}"
fuzzy_select_or_fallback "Select a Google Cloud Region" "${region_options[@]}"
region_choice=$INTERACTIVE_SELECTION

if [ $region_choice -eq 5 ]; then # Custom region
Expand All @@ -489,7 +547,7 @@ get_deployment_config() {
declare -a memory_values=("512Mi" "1Gi" "2Gi" "custom")
declare -a cpu_values=("1" "1" "2" "custom")

interactive_select "Select Resource Configuration:" "${resource_options[@]}"
fuzzy_select_or_fallback "Select Resource Configuration" "${resource_options[@]}"
resource_choice=$INTERACTIVE_SELECTION

if [ $resource_choice -eq 3 ]; then # Custom
Expand Down Expand Up @@ -737,4 +795,4 @@ main() {
}

# Run main function
main
main
Loading
Loading