generated from nhcarrigan/template
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
## 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>
270 lines
8.4 KiB
Python
270 lines
8.4 KiB
Python
"""Generate markdown participant and leader profile files for the cohort.
|
|
|
|
Reads all evaluation data files and produces two markdown files summarising
|
|
each member's tech stack, availability, proficiency, and leadership assessment.
|
|
|
|
Data files (place in data/):
|
|
- discord_verification.json Discord ID verification results (from verify_discord.py)
|
|
- proficiency_evaluations.json Proficiency scores (from evaluate_technical_proficiency.py)
|
|
- availability_analysis.json Availability UTC blocks (from analyse_availability.py)
|
|
- leadership_candidates.json List of applicants who expressed interest in leading
|
|
- leadership_evaluations.json Leadership assessment scores
|
|
|
|
Outputs (written to data/):
|
|
- participants.md Markdown profile for each participant
|
|
- leaders.md Markdown profile for each leader candidate
|
|
|
|
Env vars:
|
|
- None
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
DATA_DIR = Path(__file__).parent.parent.parent / "data"
|
|
|
|
BLOCK_EMOJIS = {"mornings": "🌅", "afternoons": "☀️", "evenings": "🌆", "nights": "🌙"}
|
|
|
|
|
|
def load_all_data():
|
|
"""Load all evaluation data files"""
|
|
with open(DATA_DIR / "discord_verification.json") as f:
|
|
verification = json.load(f)
|
|
|
|
with open(DATA_DIR / "proficiency_evaluations.json") as f:
|
|
proficiency = json.load(f)
|
|
|
|
with open(DATA_DIR / "availability_analysis.json") as f:
|
|
availability = json.load(f)
|
|
|
|
with open(DATA_DIR / "leadership_candidates.json") as f:
|
|
candidates = json.load(f)
|
|
|
|
with open(DATA_DIR / "leadership_evaluations.json") as f:
|
|
leadership = json.load(f)
|
|
|
|
return verification, proficiency, availability, candidates, leadership
|
|
|
|
|
|
def build_lookup_dicts(verification, proficiency, availability, leadership):
|
|
"""Build lookup dictionaries by discord_id"""
|
|
verified_usernames = {v[0]: v[1] for v in verification["verified"]}
|
|
|
|
prof_by_id = {p["discord_id"]: p for p in proficiency}
|
|
|
|
avail_by_id = {a["discord_id"]: a for a in availability}
|
|
|
|
lead_by_id = {l["discord_id"]: l for l in leadership}
|
|
|
|
return verified_usernames, prof_by_id, avail_by_id, lead_by_id
|
|
|
|
|
|
def format_availability_blocks(blocks):
|
|
"""Format availability blocks with emojis"""
|
|
if not blocks:
|
|
return "No consistent availability"
|
|
|
|
formatted = []
|
|
for block in ["mornings", "afternoons", "evenings", "nights"]:
|
|
if block in blocks:
|
|
formatted.append(f"{BLOCK_EMOJIS[block]} {block.capitalize()}")
|
|
return ", ".join(formatted)
|
|
|
|
|
|
def format_tech_stack(tech_stack):
|
|
"""Format tech stack list"""
|
|
if not tech_stack:
|
|
return "Not specified"
|
|
return ", ".join(sorted(tech_stack))
|
|
|
|
|
|
def generate_participants_md(
|
|
non_leader_ids, verified_usernames, prof_by_id, avail_by_id
|
|
):
|
|
"""Generate participants.md for non-leaders"""
|
|
lines = [
|
|
"# Cohort Participants",
|
|
"",
|
|
f"**Total Participants**: {len(non_leader_ids)}",
|
|
"",
|
|
"---",
|
|
"",
|
|
]
|
|
|
|
beginner_count = 0
|
|
intermediate_count = 0
|
|
advanced_count = 0
|
|
|
|
for discord_id in sorted(non_leader_ids):
|
|
if discord_id not in verified_usernames:
|
|
continue
|
|
|
|
username = verified_usernames.get(discord_id, "Unknown")
|
|
prof = prof_by_id.get(discord_id, {})
|
|
avail = avail_by_id.get(discord_id, {})
|
|
|
|
proficiency = prof.get("final_proficiency", "unknown")
|
|
tech_stack = prof.get("tech_stack", [])
|
|
blocks = avail.get("available_blocks", [])
|
|
notes = prof.get("notes", [])
|
|
|
|
if proficiency == "beginner":
|
|
beginner_count += 1
|
|
elif proficiency == "intermediate":
|
|
intermediate_count += 1
|
|
elif proficiency == "advanced":
|
|
advanced_count += 1
|
|
|
|
lines.append(f"## {discord_id}")
|
|
lines.append(f"**Username**: @{username}")
|
|
lines.append(f"**Technical Proficiency**: {proficiency.capitalize()}")
|
|
lines.append(f"**Tech Stack**: {format_tech_stack(tech_stack)}")
|
|
lines.append(f"**Availability**: {format_availability_blocks(blocks)}")
|
|
if notes:
|
|
lines.append(f"**Notes**: {', '.join(notes)}")
|
|
lines.append("")
|
|
|
|
verified_count = len([d for d in non_leader_ids if d in verified_usernames])
|
|
summary = [
|
|
"# Cohort Participants",
|
|
"",
|
|
f"**Total Participants**: {verified_count}",
|
|
"",
|
|
"### Proficiency Breakdown",
|
|
f"- Beginner: {beginner_count}",
|
|
f"- Intermediate: {intermediate_count}",
|
|
f"- Advanced: {advanced_count}",
|
|
"",
|
|
"---",
|
|
"",
|
|
]
|
|
|
|
return "\n".join(summary + lines[6:])
|
|
|
|
|
|
def leadership_fit_label(score):
|
|
"""Convert leadership score to label"""
|
|
if score >= 6:
|
|
return "Excellent"
|
|
elif score >= 4:
|
|
return "Good"
|
|
elif score >= 2:
|
|
return "Adequate"
|
|
else:
|
|
return "Needs Review"
|
|
|
|
|
|
def generate_leaders_md(
|
|
leader_ids, verified_usernames, prof_by_id, avail_by_id, lead_by_id
|
|
):
|
|
"""Generate leaders.md for leadership candidates"""
|
|
verified_leaders = [id for id in leader_ids if id in verified_usernames]
|
|
|
|
lines = [
|
|
"# Cohort Leaders",
|
|
"",
|
|
f"**Total Leaders**: {len(verified_leaders)}",
|
|
"",
|
|
"---",
|
|
"",
|
|
]
|
|
|
|
sorted_leaders = sorted(
|
|
verified_leaders,
|
|
key=lambda x: lead_by_id.get(x, {}).get("leadership_score", 0),
|
|
reverse=True,
|
|
)
|
|
|
|
for discord_id in sorted_leaders:
|
|
username = verified_usernames.get(discord_id, "Unknown")
|
|
prof = prof_by_id.get(discord_id, {})
|
|
avail = avail_by_id.get(discord_id, {})
|
|
lead = lead_by_id.get(discord_id, {})
|
|
|
|
proficiency = prof.get("final_proficiency", "unknown")
|
|
tech_stack = prof.get("tech_stack", [])
|
|
blocks = avail.get("available_blocks", [])
|
|
|
|
leadership_score = lead.get("leadership_score", 0)
|
|
leadership_fit = lead.get("leadership_fit", "unknown")
|
|
leadership_notes = lead.get("notes", [])
|
|
prof_notes = prof.get("notes", [])
|
|
|
|
lines.append(f"## {discord_id}")
|
|
lines.append(f"**Username**: @{username}")
|
|
fit = leadership_fit.capitalize()
|
|
lines.append(f"**Leadership Fit**: {fit} (Score: {leadership_score})")
|
|
lines.append(f"**Technical Proficiency**: {proficiency.capitalize()}")
|
|
lines.append(f"**Tech Stack**: {format_tech_stack(tech_stack)}")
|
|
lines.append(f"**Availability**: {format_availability_blocks(blocks)}")
|
|
if leadership_notes:
|
|
lines.append(f"**Leadership Notes**: {', '.join(leadership_notes)}")
|
|
if prof_notes:
|
|
lines.append(f"**Technical Notes**: {', '.join(prof_notes)}")
|
|
lines.append("")
|
|
|
|
excellent = sum(
|
|
1
|
|
for id in verified_leaders
|
|
if lead_by_id.get(id, {}).get("leadership_fit") == "excellent"
|
|
)
|
|
good = sum(
|
|
1
|
|
for id in verified_leaders
|
|
if lead_by_id.get(id, {}).get("leadership_fit") == "good"
|
|
)
|
|
adequate = sum(
|
|
1
|
|
for id in verified_leaders
|
|
if lead_by_id.get(id, {}).get("leadership_fit") == "adequate"
|
|
)
|
|
|
|
summary = [
|
|
"# Cohort Leaders",
|
|
"",
|
|
f"**Total Leaders**: {len(verified_leaders)}",
|
|
"",
|
|
"### Leadership Fit Breakdown",
|
|
f"- Excellent: {excellent}",
|
|
f"- Good: {good}",
|
|
f"- Adequate: {adequate}",
|
|
"",
|
|
"---",
|
|
"",
|
|
]
|
|
|
|
return "\n".join(summary + lines[6:])
|
|
|
|
|
|
def main():
|
|
verification, proficiency, availability, candidates, leadership = load_all_data()
|
|
|
|
verified_usernames, prof_by_id, avail_by_id, lead_by_id = build_lookup_dicts(
|
|
verification, proficiency, availability, leadership
|
|
)
|
|
|
|
leader_ids = set(candidates["leaders"])
|
|
non_leader_ids = set(candidates["non_leaders"])
|
|
|
|
verified_ids = set(verified_usernames.keys())
|
|
leader_ids = leader_ids & verified_ids
|
|
non_leader_ids = non_leader_ids & verified_ids
|
|
|
|
participants_md = generate_participants_md(
|
|
non_leader_ids, verified_usernames, prof_by_id, avail_by_id
|
|
)
|
|
with open(DATA_DIR / "participants.md", "w") as f:
|
|
f.write(participants_md)
|
|
print(f"Generated participants.md with {len(non_leader_ids)} participants")
|
|
|
|
leaders_md = generate_leaders_md(
|
|
leader_ids, verified_usernames, prof_by_id, avail_by_id, lead_by_id
|
|
)
|
|
with open(DATA_DIR / "leaders.md", "w") as f:
|
|
f.write(leaders_md)
|
|
print(f"Generated leaders.md with {len(leader_ids)} leaders")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|