Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0f56f5b
fix: align webhook status storage
tim48-robot Dec 31, 2025
9441371
fix: show org last sync for zero-contribution users
tim48-robot Dec 31, 2025
3897c01
feat(shared): migrate firestore storage to organization-scoped paths
tim48-robot Jan 12, 2026
3616127
feat(notifications): support multiple discord servers per github orga…
tim48-robot Jan 12, 2026
ed20a40
fix(pr-review): resolve asyncio event loop errors and add aiohttp dep…
tim48-robot Jan 12, 2026
cc0bb72
security(auth): protect debug endpoint and add setup success notifica…
tim48-robot Jan 12, 2026
30b92b1
fix(bot): improve setup reminders and update config templates
tim48-robot Jan 12, 2026
757e5fa
fix(firestore): restore notification_config while maintaining SaaS logic
tim48-robot Jan 12, 2026
faf7b1e
fix: resolve NoneType error in webhook status and revert stylistic re…
tim48-robot Jan 12, 2026
660db09
fix: resolve resource leaks and error handling in notification system
tim48-robot Jan 13, 2026
f68b617
refactor: remove on_ready setup reminder and simplify footer
tim48-robot Jan 13, 2026
0e83019
fix: validate setup before webhooks and show per-server timestamps
tim48-robot Jan 13, 2026
cecaf74
feat: add GitHub webhook handler for SaaS PR automation
tim48-robot Jan 13, 2026
0a94ccd
fix: include pr_review in Docker build for webhook automation
tim48-robot Jan 13, 2026
ae4b95b
fix: use try/except import pattern for pr_review package compatibility
tim48-robot Jan 13, 2026
cba969b
fix: install pr_review dependencies from its own requirements.txt
tim48-robot Jan 13, 2026
a750fd1
fix: update all pr_review utils to use try/except import pattern
tim48-robot Jan 13, 2026
36c0acc
fix: prevent OAuth session memory leak and extend setup state expiration
tim48-robot Jan 30, 2026
02830f6
refactor: remove /set_webhook command (PR automation disabled)
tim48-robot Jan 30, 2026
e0456a0
feat: role hierarchy validation + hide PR automation commands
tim48-robot Jan 30, 2026
54b09d2
style: premium UI redesign for all setup flow pages with elegant 500p…
tim48-robot Feb 9, 2026
46127eb
feat: add cross-thread communication infrastructure
tim48-robot Feb 12, 2026
94f9e1b
refactor: async offload all Firestore calls for SaaS responsiveness
tim48-robot Feb 12, 2026
b89bf4d
fix: replace global lock + polling with per-user event-driven /link
tim48-robot Feb 12, 2026
6a5d9ea
docs: add MAINTAINER.md with environment and feature re-enablement gu…
tim48-robot Feb 12, 2026
6e98612
security: add SECRET_KEY to .env.example
tim48-robot Feb 12, 2026
6e41cb1
refactor: optimize analytics responsiveness and update maintainer sec…
tim48-robot Feb 12, 2026
1062fdf
fix: timezone-aware datetimes, deprecated utcnow(), dead code, and do…
tim48-robot Feb 12, 2026
5720f11
fix: timezone-aware datetimes, deprecated utcnow(), dead code, and do…
tim48-robot Feb 12, 2026
55ad0b6
fix(/sync): use REPO_OWNER installation token, improve UX, add /help …
tim48-robot Feb 20, 2026
dd01464
fix coderabbit bug: log warning when sync metadata write fails
tim48-robot Feb 20, 2026
8eb152b
fix: auto-clean duplicate REPOSITORY STATS voice categories
tim48-robot Feb 20, 2026
d4613b2
fix(env_validator): allow .env with fewer lines than .env.example
tim48-robot Feb 20, 2026
9426cd9
fix(deploy.sh): include REPO_OWNER/REPO_NAME/WORKFLOW_REF in .env edi…
tim48-robot Feb 20, 2026
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
141 changes: 141 additions & 0 deletions MAINTAINER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Maintainer Guide

This document explains how to manage the environment variables and how to re-enable features that are currently disabled (commented out) on the `feature/saas-ready` branch.

## Multi-Tenant Architecture

### How GitHub Org ↔ Discord Server Works

- **One GitHub org can be connected to multiple Discord servers.**
- Each Discord server stores its own config in `discord_servers/{guild_id}` with a `github_org` field.
- Org-scoped data (repo stats, PR config, monitoring) is stored under `organizations/{github_org}/...` and shared across all Discord servers connected to the same org.
- The GitHub App only needs to be **installed once per org** on GitHub.

### Setup Flow

| Scenario | Steps | Approval needed? |
|---|---|---|
| **Owner/Admin runs `/setup`** | `/setup` → click link → Install on GitHub → done | No (owner installs directly) |
| **Member runs `/setup`** | `/setup` → click link → "Request" on GitHub → owner approves from GitHub notification → owner runs `/setup` in Discord | Yes (first time only) |
| **Second Discord server, same org** | Anyone runs `/setup` → click link → app already installed → done | No (already installed) |

### Key Points

- Only the **first installation** per GitHub org requires the org owner to approve (if initiated by a non-owner member).
- Once a GitHub App is installed on an org, **any Discord server** can connect to it via `/setup` without needing another approval.
- `/add_repo` and `/remove_repo` are **scoped to the configured org** — you can only monitor repos within your connected GitHub organization.

### `/sync` — Per-Server Cooldown, Shared Pipeline

- The **12-hour cooldown is per Discord server** (keyed on `guild_id`). Each server stores its own `last_sync_at` + `last_sync_status` in `discord_servers/{guild_id}/config`.
- Two Discord servers connected to the **same GitHub org** each have independent cooldowns. If both trigger `/sync`, the pipeline runs twice on the same org's data — wasteful but harmless.
- The pipeline itself writes to `organizations/{github_org}/...`, which is shared. Running it twice back-to-back on the same org is safe (idempotent write).
- `trigger_initial_sync()` always bypasses the cooldown (`respect_cooldown=False`) so the first sync after `/setup` always fires.

### Voice Channel Stats — Per-Guild, Updated by Pipeline

- Each Discord server gets its own `REPOSITORY STATS` voice-channel category. The pipeline iterates over **all guilds** the bot is in and updates each one.
- The channels reflect org-level metrics (stars, forks, contributors, PRs, issues, commits) fetched from `organizations/{github_org}/...`.
- **Duplicate category root cause:** `discord.utils.get()` only returns the first matching category. If `/setup_voice_stats` and the pipeline's `_update_channels_for_guild` both run near-simultaneously (e.g., first deploy + immediate pipeline trigger), both find no existing category and both create one, resulting in two `REPOSITORY STATS` categories. The fix: scan for *all* categories with that name, keep the first, delete the rest. `/setup_voice_stats` now also detects and cleans up duplicates automatically.

---

## Environment Variables

### Core Variables (Required for Launch)
These are already in your `.env.example`:
- `DISCORD_BOT_TOKEN`: The bot token from Discord Developer Portal.
- `DISCORD_BOT_CLIENT_ID`: The client ID of your Discord bot.
- `GITHUB_CLIENT_ID`: OAuth client ID from your GitHub App.
- `GITHUB_CLIENT_SECRET`: OAuth client secret from your GitHub App.
- `OAUTH_BASE_URL`: The public URL where the bot is hosted (e.g., `https://your-bot.cloudfunctions.net`).
- `GITHUB_APP_ID`: Your GitHub App ID.
- `GITHUB_APP_PRIVATE_KEY_B64`: Your GitHub App's private key, encoded in Base64.
- `GITHUB_APP_SLUG`: The URL-friendly name of your GitHub App.

### Security Variables (Recommended for Production)
- `SECRET_KEY`: Used by Flask to sign session cookies.
- **Usage**: Encrypting the `discord_user_id` during the `/link` flow.
- **Manual Check**: If you change this key while a user is mid-authentication, their session will be invalidated, and they will see "Authentication failed: No Discord user session".
- **Generation**: `python3 -c "import secrets; print(secrets.token_hex(32))"`

### Feature-Specific Variables (Optional/Disabled)
- `GITHUB_WEBHOOK_SECRET`: Required ONLY for PR automation. Used to verify that webhooks are actually coming from GitHub.
- `GITHUB_TOKEN`: Original personal access token (largely replaced by GitHub App identity).
- `REPO_OWNER`: The GitHub account/org that **owns the `disgitbot` fork** where the pipeline workflow lives. Defaults to `ruxailab`. Must be set if you are running the bot from a fork.
- `REPO_NAME`: The repository name hosting the pipeline. Defaults to `disgitbot`.
- `WORKFLOW_REF`: The branch/tag to dispatch the workflow on. Defaults to `main`. Set this if your active branch is not `main` (e.g. `feature/saas-ready` during testing).

---

## Setting Up `/sync` (Manual Pipeline Trigger)

The `/sync` command lets Discord admins manually trigger the GitHub Actions data pipeline. It uses the GitHub App's installation token to dispatch a workflow on `REPO_OWNER/REPO_NAME`.

### Required Steps

**1. Set the correct env vars in `.env`:**
```
REPO_OWNER=<org-or-user-that-owns-the-disgitbot-repo>
REPO_NAME=disgitbot
WORKFLOW_REF=main # or your branch name during testing
```

**2. Enable Actions permission on the GitHub App:**
1. Go to `github.com/organizations/{your-org}/settings/apps/{your-app-slug}`
2. Click **Permissions & events** → **Repository permissions**
3. Find **Actions** (first item — "Workflows, workflow runs and artifacts")
4. Change it from `No access` → **Read & write**
5. Save changes

**3. Accept the updated permissions:**
After saving, GitHub will notify all existing installations to accept the new permission. Go to `github.com/settings/installations` (or org equivalent) and approve the updated permissions for the installation on `REPO_OWNER`.

> **Note:** `REPO_OWNER` must be the account where the GitHub App is **installed** (not just where it was created). If you forked the repo to a different org/account, install the App there first.

---

## Re-enabling PR Automation

PR automation is currently commented out to simplify the SaaS experience. To re-enable it:

### 1. Uncomment Command Registration
In `discord_bot/src/bot/commands/admin_commands.py`:
```python
# In register_commands():
self.bot.tree.add_command(self._add_reviewer_command())
self.bot.tree.add_command(self._remove_reviewer_command())
```

In `discord_bot/src/bot/commands/notification_commands.py`:
```python
# In register_commands():
# self.bot.tree.add_command(self._webhook_status_command())
```

### 2. Configure GitHub App Webhooks
1. Go to your GitHub App settings.
2. Enable **Webhooks**.
3. **Webhook URL**: `{OAUTH_BASE_URL}/github-webhook`
4. **Webhook Secret**: Set a random string and update `GITHUB_WEBHOOK_SECRET` in your `.env`.
5. **Permissions & Events**:
- Push: `read & write` (checks)
- Pull Requests: `read & write`
- Repository metadata: `read-only`
- Subscribe to: `Pull request`, `Push`, `Workflow run`.

### 3. Performance & Responsiveness
- **Async I/O**: Use `await asyncio.to_thread` for all Firestore and synchronous network calls.
- **CPU-Bound Tasks**: Avoid long-running computations (like image generation) in the main thread. Wrap them in `asyncio.to_thread` to keep the bot responsive.
- **Shared Object Model**: Use the `shared.bot_instance` pattern for cross-thread communication between Flask and Discord.

### 4. Async Architecture Pattern
Always use this pattern for blocking calls:

```python
# Offload to thread to keep event loop free
result = await asyncio.to_thread(get_document, 'collection', 'doc_id', discord_server_id)

# Offload CPU-bound calculations
buffer = await asyncio.to_thread(generate_complex_chart, data)
```
50 changes: 6 additions & 44 deletions discord_bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,21 @@ cp discord_bot/config/.env.example discord_bot/config/.env

**Your `.env` file needs these values:**
- `DISCORD_BOT_TOKEN=` (Discord bot authentication)
- `GITHUB_TOKEN=` (Github API access)
- `GITHUB_CLIENT_ID=` (GitHub OAuth app ID)
- `GITHUB_CLIENT_SECRET=` (GitHub OAuth app secret)
- `OAUTH_BASE_URL=` (Your Cloud Run URL - set in Step 3)
- `DISCORD_BOT_CLIENT_ID=` (Discord application ID)
- `GITHUB_APP_ID=` (GitHub App ID)
- `GITHUB_APP_PRIVATE_KEY_B64=` (GitHub App private key, base64)
- `GITHUB_APP_SLUG=` (GitHub App slug)
- `OAUTH_BASE_URL=` (Your Cloud Run URL - set in Step 4)
- `REPO_OWNER=` (Owner of the Disgitbot repo that hosts the workflow dispatch. Ex: ruxailab)

**Additional files you need:**
- `discord_bot/config/credentials.json` (Firebase/Google Cloud credentials)

**GitHub repository secrets you need to configure:**
Go to your GitHub repository → Settings → Secrets and variables → Actions → Click "New repository secret" for each:
- `DISCORD_BOT_TOKEN`
- `GH_TOKEN`
- `GOOGLE_CREDENTIALS_JSON`
- `REPO_OWNER`
- `CLOUD_RUN_URL`
- `GH_APP_ID`
- `GH_APP_PRIVATE_KEY_B64`
Expand All @@ -150,7 +147,6 @@ If you plan to run GitHub Actions from branches other than `main`, also add the
- `DEV_GOOGLE_CREDENTIALS_JSON`
- `DEV_CLOUD_RUN_URL`

> The workflows only reference `GH_TOKEN`, so you can reuse the same PAT for all branches.

---

Expand Down Expand Up @@ -250,25 +246,7 @@ If you plan to run GitHub Actions from branches other than `main`, also add the
- **Add to GitHub Secrets:** Create secret named `GOOGLE_CREDENTIALS_JSON` with the base64 string
- *(Do this for non-main branches)* Create another secret named `DEV_GOOGLE_CREDENTIALS_JSON` with the same base64 string so development branches can run GitHub Actions.

### Step 3: Get GITHUB_TOKEN (.env) + GH_TOKEN (GitHub Secret)

**What this configures:**
- `.env` file: `GITHUB_TOKEN=your_token_here`
- GitHub Secret: `GH_TOKEN`

**What this does:** Allows the bot to access dispatch the Github Actions Workflow

1. **Go to GitHub Token Settings:** https://github.com/settings/tokens
2. **Create New Token:**
- Click "Generate new token" → "Generate new token (classic)"
3. **Set Permissions:**
- Check only: [x] `repo` (this gives full repository access)
4. **Generate and Save:**
- Click "Generate token" → Copy the token
- **Add to `.env`:** `GITHUB_TOKEN=your_token_here`
- **Add to GitHub Secrets:** Create secret named `GH_TOKEN`

### Step 4: Get Cloud Run URL (Placeholder Deployment)
### Step 3: Get Cloud Run URL (Placeholder Deployment)

**What this configures:**
- `.env` file: `OAUTH_BASE_URL=YOUR_CLOUD_RUN_URL`
Expand Down Expand Up @@ -305,7 +283,7 @@ If you plan to run GitHub Actions from branches other than `main`, also add the
- **Example:** `https://discord-bot-abcd1234-uc.a.run.app/setup`
- Click **Save Changes**

### Step 5: Get GITHUB_CLIENT_ID (.env) + GITHUB_CLIENT_SECRET (.env)
### Step 4: Get GITHUB_CLIENT_ID (.env) + GITHUB_CLIENT_SECRET (.env)

**What this configures:**
- `.env` file: `GITHUB_CLIENT_ID=your_client_id`
Expand All @@ -318,7 +296,7 @@ If you plan to run GitHub Actions from branches other than `main`, also add the
- Click "New OAuth App"
3. **Fill in Application Details:**
- **Application name:** `Your Bot Name` (anything you want)
- **Homepage URL:** `YOUR_CLOUD_RUN_URL` (from Step 4)
- **Homepage URL:** `YOUR_CLOUD_RUN_URL` (from Step 3)
- **Authorization callback URL:** `YOUR_CLOUD_RUN_URL/login/github/authorized`

**Example URLs:** If your Cloud Run URL is `https://discord-bot-abcd1234-uc.a.run.app`, then:
Expand All @@ -331,7 +309,7 @@ If you plan to run GitHub Actions from branches other than `main`, also add the
- 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)
### Step 5: 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=...`
Expand Down Expand Up @@ -368,21 +346,6 @@ If you plan to run GitHub Actions from branches other than `main`, also add the

**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:**
- `.env` file: `REPO_OWNER=your_org_name`
- GitHub Secret: `REPO_OWNER`

**What this does:** Tells the bot which Disgitbot repo owns the GitHub Actions workflow (used for workflow dispatch). The org you track comes from GitHub App installation during `/setup`.

1. **Find the Disgitbot repo owner:**
- Example repo: `https://github.com/ruxailab/disgitbot`
- The owner is the first path segment (`ruxailab`)
2. **Set in Configuration:**
- **Add to `.env`:** `REPO_OWNER=your_repo_owner` (example: `REPO_OWNER=ruxailab`)
- **Add to GitHub Secrets:** Create secret named `REPO_OWNER` with the same value
- **Important:** Use ONLY the organization name, NOT the full URL

---

Expand Down Expand Up @@ -770,7 +733,6 @@ async def link(interaction: discord.Interaction):
# Check required environment variables
required_vars = [
"DISCORD_BOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"OAUTH_BASE_URL" # ← This is your Cloud Run URL
Expand Down
6 changes: 4 additions & 2 deletions discord_bot/config/.env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
DISCORD_BOT_TOKEN=
GITHUB_TOKEN=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
REPO_OWNER=
OAUTH_BASE_URL=
DISCORD_BOT_CLIENT_ID=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY_B64=
GITHUB_APP_SLUG=
SECRET_KEY=
REPO_OWNER=
REPO_NAME=
WORKFLOW_REF=
11 changes: 9 additions & 2 deletions discord_bot/deployment/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ RUN apt-get update && \
# Copy requirements first to leverage Docker cache
COPY requirements.txt .

# Copy only requirements files first to leverage Docker layer cache
COPY pr_review/requirements.txt ./pr_review/requirements.txt

# Upgrade pip to latest version to avoid upgrade notices
RUN pip install --upgrade pip
RUN pip install --no-cache-dir --upgrade pip

# Install dependencies from both requirements files
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt -r pr_review/requirements.txt

RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
# Copy pr_review package (copied into build context by deploy script)
COPY pr_review ./pr_review

# Create config directory and empty credentials file (will be overwritten by volume mount)
RUN mkdir -p /app/config && echo "{}" > /app/config/credentials.json
Expand Down
Loading
Loading