Skip to content

fix(teams): normalize github_org to a slug; readable error when the GitHub org lookup fails - #286

Open
gregv wants to merge 1 commit into
developfrom
fix/github-org-slug-normalization
Open

gregv wants to merge 1 commit into
developfrom
fix/github-org-slug-normalization

Conversation

@gregv

@gregv gregv commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Why

"Approve Team" on the test event fall-2026 returned a 500. Fly logs:

api/teams/teams_service.py:985 approve_team → create_github_repo(...)
common/utils/github.py:31   org = g.get_organization(org_name)
github.GithubException.UnknownObjectException: 404 {"message": "Not Found"}

The event's github_org is stored as https://github.com/Opportunity-Hack-2026 (a pasted URL). It was handed verbatim to PyGithub, which requested /orgs/https://github.com/Opportunity-Hack-2026 → 404. The org itself exists (/orgs/Opportunity-Hack-2026 → 200), and GITHUB_TOKEN is fine (a bad token gives 401). The exception escaped because create_github_repo only wrapped create_repo, and approve_team only catches ValueError.

What

  • common/utils/validators.py: new normalize_github_org(raw) — strips https://github.com/, www., a leading @, any trailing path, and whitespace (mirrors the frontend's githubOrgSlug). validate_hackathon_data_partial now stores the normalized slug; save_hackathon uses its output for both create and update, so every save path is covered.
  • api/teams/teams_service.py::approve_team: normalizes the stored value too (events saved before this fix) and returns an actionable message when it's empty.
  • common/utils/github.py: get_github_organization(g, org_name) wraps the lookup and raises ValueError whose message distinguishes 404 (bad slug / no such org) from 401/403 (token). create_github_repo resolves the org once and passes it into does_repo_exist (was a second get_organization call). approve_team's existing except ValueError turns this into 200 {success: false, message}.
  • Tests: test/common/utils/test_validators.py (normalizer table + partial-validator behaviour).

Not changed (follow-up if wanted): api/leaderboard/leaderboard_service.py uses the raw hackathon["github_org"] as a Firestore document id, so a legacy URL value there would still break until that event is re-saved.

Frontend companion PR (normalizes on blur + shows the success:false message, which the admin UI currently swallows): opportunity-hack/frontend-ohack.dev#369.

Test plan

  1. PYTHONPATH=. python -m pytest test/common/utils/test_validators.py → 49 passed.
  2. Deploy to test, then PATCH /api/messages/hackathon for fall-2026 with github_org: "https://github.com/Opportunity-Hack-2026/"GET /api/messages/hackathon/fall-2026 returns "github_org": "Opportunity-Hack-2026".
  3. Without re-saving the org (leave the URL value in Firestore): /admin/hackathons/fall-2026?section=teams → open the adadad team → Nonprofit Assignment → pick nonprofit → Approve Team → succeeds; repo adadad-NMTSAEducationPlatfo appears in github.com/Opportunity-Hack-2026; no traceback in Fly logs.
  4. Set github_org to no-such-org-xyz-123 → Approve Team → HTTP 200 {"success": false, "message": "Error creating GitHub repo: GitHub organization 'no-such-org-xyz-123' not found on GitHub — check …"}; no 500.
  5. Clear github_org → Approve Team → {"success": false, "message": "Error: this event has no GitHub organization set. Add it under Admin → …"}.

🤖 Generated with Claude Code

…itHub org lookup fails

POST /api/team/approve 500'd on fall-2026 (test): the event's github_org was
stored as "https://github.com/Opportunity-Hack-2026" and approve_team passed
it verbatim to PyGithub's get_organization -> UnknownObjectException 404,
which create_github_repo did not catch (only create_repo was wrapped) and
approve_team only catches ValueError.

- validators.normalize_github_org(): strip https://github.com/, www., @,
  trailing path and whitespace (mirrors the frontend's githubOrgSlug).
  validate_hackathon_data_partial now stores the normalized slug, which
  covers both create and update since save_hackathon uses its output.
- approve_team: normalize the stored value too (events saved before this
  fix) and return an actionable message when it is empty.
- common/utils/github.py: get_github_organization() wraps the lookup and
  raises ValueError distinguishing 404 (bad slug / no such org) from 401/403
  (token). create_github_repo resolves the org once and passes it into
  does_repo_exist instead of looking it up twice.
- Tests for the normalizer and the partial validator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant