Files
ephemere/python/cohort/assign_cohort_role.py
T
hikari ec58c9c843
CI / dependency-pin-check-typescript (push) Successful in 5s
CI / dependency-pin-check-python (push) Successful in 4s
CI / python (push) Successful in 9m28s
CI / typescript (push) Successful in 9m42s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m39s
feat: reorganise bash scripts and add comprehensive documentation (#6)
## Summary

This PR completes the bash script restructuring and adds comprehensive documentation across all script categories.

### Bash Restructuring

- Moved cohort shell scripts (`remove_github_members.sh`, `update_github_teams.sh`) from `python/cohort/` into a new `bash/cohort/` directory
- Moved existing bash utilities (`add-keys-to-git.sh`, `fix-yubikey-perms.sh`, `list-yubikey-ssh-keys.sh`) into a new `bash/yubikey/` subdirectory
- Updated `run.sh` to support **Bash** as a third language option alongside TypeScript and Python
  - Bash scripts are run directly (no 1Password secret injection needed)
  - Category discovery and script listing works the same as for TS/Python
  - Removed dead "Root Scripts" logic that was no longer needed

### Documentation

Added `README.md` files for all script categories that were missing them:

- `bash/cohort/README.md` — cohort GitHub team management scripts
- `bash/yubikey/README.md` — YubiKey SSH key and permission utilities
- `typescript/src/crowdin/README.md` — Crowdin translation management scripts
- `typescript/src/discord/README.md` — Discord bot utility scripts
- `typescript/src/discourse/README.md` — Discourse forum management scripts
- `typescript/src/gitea/README.md` — Gitea bulk repository operation scripts
- `typescript/src/github/README.md` — GitHub API interaction scripts
- `typescript/src/music/README.md` — Music file metadata tools
- `typescript/src/s3/README.md` — S3-compatible object storage scripts
- `typescript/src/security/README.md` — Security analysis and reporting scripts
- `python/cohort/README.md` — Updated to remove moved shell scripts, fix usage commands

Also updated project-level docs:

- **`README.md`** — Corrected project structure, fixed running instructions (removed references to non-existent `make run-ts`/`make run-py` targets), added Bash prerequisites
- **`CLAUDE.md`** — Updated project overview, structure, development standards, and script-adding guides to reflect the current state of the project

 This PR was created with help from Hikari~ 🌸

Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Reviewed-on: #6
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-02-23 20:18:41 -08:00

90 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Assign the Cohort role to all 155 participants.
Respects Discord rate limits with proper backoff and retry logic.
"""
import json
import os
import time
from pathlib import Path
import requests
DATA_DIR = Path(__file__).parent.parent.parent / "data"
BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
GUILD_ID = "692816967895220344"
COHORT_ROLE_ID = "1464314780935258112"
BASE_URL = "https://discord.com/api/v10"
HEADERS = {"Authorization": f"Bot {BOT_TOKEN}", "Content-Length": "0"}
def assign_role_with_retry(user_id: str, role_id: str, max_retries: int = 5) -> bool:
url = f"{BASE_URL}/guilds/{GUILD_ID}/members/{user_id}/roles/{role_id}"
for attempt in range(max_retries):
response = requests.put(url, headers=HEADERS)
if response.status_code == 204:
return True
elif response.status_code == 429:
# Check headers first, fall back to JSON body
retry_after = response.headers.get("Retry-After")
if retry_after is None:
retry_after = response.headers.get("X-RateLimit-Reset-After")
if retry_after is None:
try:
retry_after = response.json().get("retry_after", 1)
except Exception:
retry_after = 1
retry_after = float(retry_after)
print(f" Rate limited! Waiting {retry_after:.2f}s before retry...")
time.sleep(retry_after)
else:
print(f" Error {response.status_code}: {response.text}")
backoff_time = (2**attempt) * 0.5
print(f" Retrying in {backoff_time:.2f}s...")
time.sleep(backoff_time)
return False
def main():
with open(DATA_DIR / "team_assignments.json") as f:
teams = json.load(f)
all_users = []
for team in teams:
all_users.extend(team["leaders"])
all_users.extend(team["participants"])
unique_users = list(dict.fromkeys(all_users))
print(f"Assigning Cohort role to {len(unique_users)} users...")
print(f"Role ID: {COHORT_ROLE_ID}")
print("-" * 50)
success_count = 0
fail_count = 0
for i, user_id in enumerate(unique_users, 1):
print(f"[{i}/{len(unique_users)}] Assigning to {user_id}...", end=" ")
if assign_role_with_retry(user_id, COHORT_ROLE_ID):
print("")
success_count += 1
else:
print("✗ FAILED")
fail_count += 1
# Small delay between requests to be nice to the API
time.sleep(0.1)
print("-" * 50)
print(f"Complete! Success: {success_count}, Failed: {fail_count}")
if __name__ == "__main__":
main()