Files
ephemere/python/cohort/README.md
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

884 lines
22 KiB
Markdown

# Cohort Scripts
Scripts for managing the NHCarrigan spring cohort programme. Covers the full lifecycle: applicant evaluation, Discord server setup, team assignment, member onboarding, activity tracking, and member removal.
Most scripts interact with the Discord API and require a `DISCORD_BOT_TOKEN`. Several also use the `gh` CLI for GitHub operations.
## Getting Started
Run scripts via the interactive runner from the project root:
```bash
make run
# Select: Python → cohort → <script>
```
Or run directly:
```bash
cd python && op run --env-file=../prod.env -- uv run python cohort/<script>.py
```
**Prerequisites:**
- Run `make install-py` to set up the Python virtual environment.
- Most scripts require `DISCORD_BOT_TOKEN` set in `prod.env`.
- Scripts that manage GitHub teams require `gh auth login` to be run first.
## Table of Contents
### Applicant Evaluation
- [verify_discord.py](#verify_discordpy)
- [evaluate_technical_proficiency.py](#evaluate_technical_proficiencypy)
- [analyse_availability.py](#analyse_availabilitypy)
- [generate_member_files.py](#generate_member_filespy)
- [generate_timeslots.py](#generate_timeslotspy)
### Server Setup
- [create_team_voice_channels.py](#create_team_voice_channelspy)
- [fix_channel_permissions.py](#fix_channel_permissionspy)
- [update_cohort_leads_permissions.py](#update_cohort_leads_permissionspy)
- [list_all_guild_roles.py](#list_all_guild_rolespy)
- [list_discord_roles.py](#list_discord_rolespy)
- [check_channel_permissions.py](#check_channel_permissionspy)
### Member Onboarding
- [send_team_messages.py](#send_team_messagespy)
- [assign_cohort_role.py](#assign_cohort_rolepy)
- [assign_team_roles.py](#assign_team_rolespy)
- [add_github_team_members.py](#add_github_team_memberspy)
### Ongoing Management
- [get_cohort_members.py](#get_cohort_memberspy)
- [check_member_status.py](#check_member_statuspy)
- [fetch_roster.py](#fetch_rosterpy)
- [update_roster_messages.py](#update_roster_messagespy)
- [send_checkin.py](#send_checkinpy)
- [send_team_checkin.py](#send_team_checkinpy)
- [send_team_messages.py](#send_team_messagespy)
### Activity Tracking
- [discord_activity_checker.py](#discord_activity_checkerpy)
- [catch_up_report.py](#catch_up_reportpy)
- [check_lengths.py](#check_lengthspy)
- [send_activity_report.py](#send_activity_reportpy)
### Member Removal
- [remove_member.py](#remove_memberpy)
- [remove_resigned_members.py](#remove_resigned_memberspy)
- [remove_discord_roles.py](#remove_discord_rolespy)
---
## verify_discord.py
Reads Discord user IDs from a markdown table and verifies each one against the Discord API, checking whether the user is a member of the freeCodeCamp guild. Handles rate limits and retries automatically.
### Usage
```bash
make run
# Select: Python → cohort → verify_discord.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `table.md` | Markdown table | Applicant data; the script reads Discord IDs from the table rows |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `discord_verification.json` | JSON object | Results grouped as `verified` (with username), `missing`, and `errors` lists |
---
## evaluate_technical_proficiency.py
Evaluates each applicant's technical proficiency by analysing their GitHub profile and self-described expertise. Scores applicants as Beginner, Intermediate, or Advanced based on public repository count, follower count, language variety, and keywords in their self-assessment text.
### Usage
```bash
make run
# Select: Python → cohort → evaluate_technical_proficiency.py
```
### Environment Variables
None. Uses the public GitHub API (unauthenticated, subject to rate limiting).
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `applicants_to_evaluate.json` | JSON array | Applicant records, each with a GitHub profile URL and a self-description field |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `proficiency_evaluations.json` | JSON array | Proficiency scores (`Beginner`/`Intermediate`/`Advanced`) and detected tech stacks per applicant |
### Notes
- Unauthenticated GitHub API requests are rate-limited to 60 per hour. For large batches of applicants, consider adding a GitHub token or running the script in smaller chunks.
---
## analyse_availability.py
Parses a markdown table of applicant availability responses with timezone information, converts local timeslot selections to UTC, and produces a JSON summary of UTC coverage across morning, afternoon, evening, and night blocks per applicant.
### Usage
```bash
make run
# Select: Python → cohort → analyse_availability.py
```
### Environment Variables
None.
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `table.md` | Markdown table | Availability responses with timezone column |
| `discord_verification.json` | JSON object | Output of `verify_discord.py` — used to link applicants to Discord usernames |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `availability_analysis.json` | JSON object | UTC block distribution (morning/afternoon/evening/night) per applicant |
---
## generate_member_files.py
Consolidates all evaluation data into two markdown files: one for participants and one for team leaders. Each entry includes the member's tech stack, availability, proficiency level, and (for leaders) their leadership assessment.
### Usage
```bash
make run
# Select: Python → cohort → generate_member_files.py
```
### Environment Variables
None.
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `discord_verification.json` | JSON object | Verified applicant Discord info |
| `proficiency_evaluations.json` | JSON array | Technical proficiency scores |
| `availability_analysis.json` | JSON object | UTC availability blocks |
| `leadership_candidates.json` | JSON array | Applicants being considered for leadership roles |
| `leadership_evaluations.json` | JSON array | Leadership assessment results |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `participants.md` | Markdown | Profile summary for each participant |
| `leaders.md` | Markdown | Profile summary for each team leader candidate |
---
## generate_timeslots.py
Generates a list of hourly timeslots for use with the [Crabfit](https://crab.fit/) scheduling tool, covering a date range at Los Angeles timezone.
### Usage
```bash
make run
# Select: Python → cohort → generate_timeslots.py
```
### Environment Variables
None.
### Data Files
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `crabfit_timeslots.json` | JSON array of ISO-format strings | One entry per hour across the configured date range |
### Notes
- The date range and timezone are hardcoded in the script. Update the start/end date constants before running.
---
## create_team_voice_channels.py
Creates private voice channels for each cohort team in a specified Discord category. Each channel is visible and joinable only by members who have that team's role.
### Usage
```bash
make run
# Select: Python → cohort → create_team_voice_channels.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Team role IDs and the target category ID are hardcoded in the script.
### Notes
- Update the team role IDs, channel names, and category ID constants in the script before running.
---
## fix_channel_permissions.py
Denies `Send Messages` and `Send Messages in Threads` permissions for the `@everyone` and `@cohort` roles on a specific Discord channel. Used to lock down a channel so only designated roles can post.
### Usage
```bash
make run
# Select: Python → cohort → fix_channel_permissions.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. The target channel ID is hardcoded in the script.
### Notes
- Update the `CHANNEL_ID` constant before running.
---
## update_cohort_leads_permissions.py
Grants the Cohort Leads role `MENTION_EVERYONE` and `PIN_MESSAGES` permissions across all 14 team channels.
### Usage
```bash
make run
# Select: Python → cohort → update_cohort_leads_permissions.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Team channel IDs are hardcoded in the script.
---
## list_all_guild_roles.py
Lists all roles in the freeCodeCamp Discord guild, highlighting team and cohort-related roles. Useful for finding role IDs to use in other scripts.
### Usage
```bash
make run
# Select: Python → cohort → list_all_guild_roles.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Output is printed to stdout.
---
## list_discord_roles.py
Lists Discord roles in the freeCodeCamp server, filtering for cohort-, leader-, and 2026-related roles.
### Usage
```bash
make run
# Select: Python → cohort → list_discord_roles.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Output is printed to stdout.
---
## check_channel_permissions.py
Audits `cohort-team-*` Discord channels to identify incorrect `Send Messages` or `Send Messages in Threads` permissions on the `@everyone` or `@cohort` roles.
### Usage
```bash
make run
# Select: Python → cohort → check_channel_permissions.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Results are printed to stdout.
---
## send_team_messages.py
Sends initial welcome and roster messages to all 14 team Discord channels, pins them, and saves the resulting message IDs, channel IDs, and role IDs to `data/team_message_ids.json`. This file is required by several other scripts.
### Usage
```bash
make run
# Select: Python → cohort → send_team_messages.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Team rosters (leaders and participants per team) |
| `applicants_to_evaluate.json` | JSON array | Applicant data used to populate roster messages |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `team_message_ids.json` | JSON object | Channel ID, pinned message ID, and role ID per team name |
### Notes
- Run this **once** at the start of the cohort. Other scripts (`send_checkin.py`, `update_roster_messages.py`, etc.) depend on `team_message_ids.json`.
---
## assign_cohort_role.py
Assigns the Cohort Discord role to all cohort participants. Reads the full list of unique member Discord IDs from `team_assignments.json` and assigns the role to each one, with exponential backoff to handle rate limits.
### Usage
```bash
make run
# Select: Python → cohort → assign_cohort_role.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Team rosters; all unique user IDs are extracted from here |
---
## assign_team_roles.py
Assigns team-specific Discord roles to all cohort participants based on their team membership in `team_assignments.json`. Uses exponential backoff for rate limit handling.
### Usage
```bash
make run
# Select: Python → cohort → assign_team_roles.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Team rosters with Discord IDs and corresponding role IDs |
---
## add_github_team_members.py
Adds cohort members to their corresponding GitHub teams in the `nhcarrigan-spring-2026-cohort` organisation. Leaders are added to both the main team and the corresponding `-leaders` sub-team; participants are added to the main team only.
### Usage
```bash
make run
# Select: Python → cohort → add_github_team_members.py
```
### Environment Variables
None. Uses the `gh` CLI for authentication (run `gh auth login` first).
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Team rosters with leader/participant distinction |
| `discord_to_github.json` | JSON object | Maps Discord user IDs to GitHub usernames |
---
## get_cohort_members.py
Fetches all Discord guild members with the Cohort role and writes their IDs, usernames, and display names to a JSON file. Also prints a formatted list to stdout.
### Usage
```bash
make run
# Select: Python → cohort → get_cohort_members.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `active_cohort_members.json` | JSON array | Objects with `id`, `username`, and `display_name` for each Cohort role member |
---
## check_member_status.py
Verifies whether specific Discord member IDs are still in the freeCodeCamp guild by querying the Discord API. Used to confirm that removed members have been successfully ejected.
### Usage
```bash
make run
# Select: Python → cohort → check_member_status.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. The member IDs to check are hardcoded in the script. Update the list before running.
---
## fetch_roster.py
Fetches pinned messages from a specific team channel to retrieve the current roster.
### Usage
```bash
make run
# Select: Python → cohort → fetch_roster.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Output is printed to stdout as JSON.
### Notes
- The target channel ID is hardcoded. Update it before running.
---
## update_roster_messages.py
Edits the pinned team roster messages in each Discord channel with the latest data from `team_assignments.json` and `discord_to_github.json`. Run this after any membership changes to keep the pinned rosters up to date.
### Usage
```bash
make run
# Select: Python → cohort → update_roster_messages.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_message_ids.json` | JSON object | Channel and message IDs per team (output of `send_team_messages.py`) |
| `team_assignments.json` | JSON object | Current team rosters |
| `discord_to_github.json` | JSON object | Discord ID → GitHub username mapping |
---
## send_checkin.py
Sends biweekly check-in prompts to all team Discord channels (except Jade Jasmine), automatically creating threads for responses. Members who do not respond face removal for inactivity.
### Usage
```bash
make run
# Select: Python → cohort → send_checkin.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_message_ids.json` | JSON object | Channel IDs per team |
---
## send_team_checkin.py
Sends a capacity check-in message to each team channel asking whether the team feels able to complete their project with their current member count, and inviting them to request support if needed.
### Usage
```bash
make run
# Select: Python → cohort → send_team_checkin.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_message_ids.json` | JSON object | Channel IDs per team |
| `team_assignments.json` | JSON object | Current team rosters (used to mention team members) |
---
## discord_activity_checker.py
Scans each team's Discord channel and threads to identify members who have not sent a message within the last 36 hours. Can optionally send notification messages directly to inactive members via the `--send` CLI flag.
### Usage
```bash
# Check only (no messages sent)
make run
# Select: Python → cohort → discord_activity_checker.py
# Check and notify inactive members
cd python && op run --env-file=../prod.env -- uv run python cohort/discord_activity_checker.py --send
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Member lists per team |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `discord_activity_report.json` | JSON object | Inactive members per team with their last message timestamp |
---
## catch_up_report.py
Generates a detailed markdown activity report covering Discord messages (in team channels and their threads) and GitHub activity (PRs, issues, comments, reviews, commits) since a configured start date. Uses async API calls for efficiency.
### Usage
```bash
make run
# Select: Python → cohort → catch_up_report.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Team rosters |
| `discord_to_github.json` | JSON object | Discord ID → GitHub username mapping |
**Output** (written to `data/`):
| File | Format | Description |
|---|---|---|
| `catch_up_report.md` | Markdown table | Activity counts per member (Discord messages + GitHub contributions) per team |
### Notes
- The report start date is hardcoded in the script. Update it before each use.
- GitHub activity is fetched via the unauthenticated public API; rate limiting may apply for large cohorts.
---
## check_lengths.py
Dry-run validation that parses `catch_up_report.md` and formats each team's data into Discord monospace table strings, checking whether any would exceed Discord's 2,000-character message limit before actually sending them.
### Usage
```bash
make run
# Select: Python → cohort → check_lengths.py
```
### Environment Variables
None.
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `catch_up_report.md` | Markdown table | Output of `catch_up_report.py` |
### Notes
- Run this after `catch_up_report.py` and before `send_activity_report.py` to catch any messages that would be rejected by Discord.
---
## send_activity_report.py
Parses `catch_up_report.md` and sends the formatted activity table for each team to its Discord channel as a monospace code block.
### Usage
```bash
make run
# Select: Python → cohort → send_activity_report.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input** (expected in `data/`):
| File | Format | Description |
|---|---|---|
| `catch_up_report.md` | Markdown table | Output of `catch_up_report.py` |
### Notes
- Run `check_lengths.py` first to verify that no messages exceed Discord's character limit.
---
## remove_member.py
Comprehensive member removal script. Given a Discord ID as a CLI argument, it:
1. Removes the member from `team_assignments.json`.
2. Removes the member from `discord_to_github.json`.
3. Prints GitHub removal instructions.
4. Removes the member's Discord Cohort and team roles.
5. Sends an announcement message to the team's Discord channel.
6. Outputs markdown notes suitable for pasting into `COHORT_NOTES.md`.
### Usage
```bash
cd python && op run --env-file=../prod.env -- uv run python cohort/remove_member.py <discord_id>
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
**Input/Output** (read and updated in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Updated: member removed from their team |
| `discord_to_github.json` | JSON object | Updated: Discord→GitHub mapping removed |
| `team_message_ids.json` | JSON object | Read: used to find the team's Discord channel ID |
---
## remove_resigned_members.py
Removes a list of resigned member Discord IDs from `team_assignments.json`, updating team rosters and reporting which teams were affected. Does not interact with Discord or GitHub.
### Usage
```bash
make run
# Select: Python → cohort → remove_resigned_members.py
```
### Environment Variables
None.
### Data Files
**Input/Output** (read and updated in `data/`):
| File | Format | Description |
|---|---|---|
| `team_assignments.json` | JSON object | Updated in place; affected teams are reported to stdout |
### Notes
- The list of Discord IDs to remove is hardcoded in the script. Update it before running.
- This script only updates the local JSON file. Run `update_roster_messages.py` and handle GitHub/Discord role removal separately.
---
## remove_discord_roles.py
Removes the Cohort and team-specific Discord roles from a hardcoded list of inactive members.
### Usage
```bash
make run
# Select: Python → cohort → remove_discord_roles.py
```
### Environment Variables
| Variable | Description |
|---|---|
| `DISCORD_BOT_TOKEN` | Discord bot token |
### Data Files
None. Member IDs and their team-to-role mappings are hardcoded in the script. Update them before running.