-
Notifications
You must be signed in to change notification settings - Fork 3
GitHub Integration
This page documents every place where Classroom 50 touches GitHub's API or authentication infrastructure — what you have to do manually in a browser, what the CLI handles for you, and the complete list of REST API calls the tooling makes.
The CLI never creates the org for you. Do the following once before running any CLI commands:
-
Create the organization at https://github.com/account/organizations/new. Free orgs work for public template repos; Team or Enterprise Cloud is required for GitHub Pages from a private repo (the
classroom50config repo is private). -
Flag your template repos as templates under each repo's
Settings → General → Template repository. Templates must also be public on Free and Team plans so students can read them (the "No permission" baseline prevents org members from reading private repos they aren't direct collaborators on). GitHub Enterprise Cloud's "internal" visibility is the exception — see GitHub's docs on internal repositories.
Org-level member privileges are locked to least-privilege automatically — gh teacher init applies them via PATCH /orgs/{org} (see the REST table below), retrying each policy individually if the combined PATCH hits a plan-gated or enterprise-locked field. After init, the only things an org member can do are create a private repository (needed so gh student accept works) and publish a public Pages site (enforced so the classroom50 config repo's site — the unauthenticated assignments.json source — keeps working); public/internal repo creation, private Pages, repo delete/transfer, visibility changes, issue deletion, team creation, dependency insights, private-repo forking, and member-invited outside collaborators are all denied. init enforces (not just allows) the Pages policy, so re-running gh teacher init resets a teacher who tightened it back to the working state. This is what makes it safe for gh student accept to leave the student as admin of their own repo (so a group founder can add teammates with gh student invite) — the dangerous repo-admin powers are defanged org-wide. The fields GitHub rejects on your plan are the one case where you'd still flip them yourself at https://github.com/organizations/<org>/settings/member_privileges; init warns per rejected field with that link.
Four member-privilege settings have no REST API and gh teacher init cannot set or even read them — apply them manually once at https://github.com/organizations/<org>/settings/member_privileges (init prints this reminder too):
- App access requests → Members only (or Disabled)
- GitHub Apps → deselect "Allow repository admins to install GitHub Apps for their repositories"
- Projects base permissions → No access
-
Branch renames → deselect "Allow repository administrators to rename branches protected by organization rules" (enabled by default on new orgs; defense-in-depth — the
classroom50-protect-submission-historyorg ruleset already protects each repo's default branch with org-admin-only bypass, so a student-admin can't rename out of that protection. Disable it as a tidy-up.)
Run once per machine or after a token rotation:
gh teacher loginThis shells out to gh auth login -s admin:org -s workflow and opens a browser to complete GitHub's OAuth device flow (a one-time code you enter at https://github.com/login/device). Neither scope is granted by a plain gh auth login: admin:org is required for org-level invitations, and workflow lets gh teacher init commit the config repo's .github/workflows/ files via the Git Data API (GitHub returns a misleading 404 on that write without it). If you skip this step and have no token at all, the CLI detects the missing token and runs the login flow automatically. If a token exists but lacks admin:org or workflow, the affected command fails with an error telling you to run gh teacher login to grant the missing scope.
OAuth scopes requested by the teacher CLI:
| Scope | Required for |
|---|---|
admin:org |
Sending org invitations, reading and removing org memberships |
workflow |
Committing the config repo's .github/workflows/ files during gh teacher init (GitHub 404s the Git Data API write without it) |
Run once per student machine:
gh student loginSame device flow as above, but with student-appropriate scopes:
| Scope | Required for |
|---|---|
read:org |
Checking and accepting org membership |
repo |
Generating private assignment repos from templates, disabling repo features, adding collaborators |
workflow |
Committing .github/workflows/autograde.yaml into the assignment repo at accept time |
gh teacher init uploads a PAT into the CLASSROOM50_COLLECT_TOKEN Actions secret of your classroom50 config repo. That PAT is what the collect-scores.yaml workflow uses to read releases from student repos across the org.
Create the PAT at https://github.com/settings/personal-access-tokens/new (or the equivalent page for your org-owned service account — a service account is strongly recommended so the token isn't tied to a personal account):
| Setting | Value |
|---|---|
| Resource owner | your teaching org |
| Repository access |
All repositories ("Only select repositories" misses student repos, which gh student accept creates on demand after the token is minted) |
| Contents | Read |
| Metadata |
Read (mandatory — GitHub auto-includes it on every fine-grained PAT; this is what lets collect-scores read group-repo collaborators, so group assignments need no extra scope) |
| Expiry | 1–366 days (fine-grained PATs support up to 1 year); set a calendar reminder to rotate before it expires |
Group assignments need no extra scope. For a group assignment the autograder runs once in the first-accepter's repo and emits a single score;
collect_scores.pythen reads that repo's collaborators to fan the score out to every group member on the roster (the owner is always credited; a non-rostered out-of-band collaborator is excluded). Listing collaborators (GET /repos/{owner}/{repo}/collaborators) requires onlyMetadata: read, which is auto-included on every fine-grained PAT and already implied by theContents: readgrant above — so the same collect token serves individual and group assignments. If the collaborator read fails for any reason, the group submission still scores for the repo owner andcollect-scoreslogs a::warning::naming the repo.
Supply the PAT to gh teacher init via the environment variable (never a flag — command-line PATs leak via shell history):
CLASSROOM50_COLLECT_TOKEN=github_pat_... gh teacher init <org>Or omit it and the CLI prompts for it with hidden TTY input. The token is encrypted with libsodium sealbox before being uploaded to GitHub and is never written to disk.
To rotate an expiring token, create a new one with the same settings and run:
CLASSROOM50_COLLECT_TOKEN=github_pat_... gh teacher rotate-collect-token <org>gh teacher init enables Pages programmatically via POST https://api.github.com/repos/{owner}/{repo}/pages and sets visibility to public via PUT https://api.github.com/repos/{owner}/{repo}/pages. The Pages site is built by the publish-pages.yaml workflow that init commits into the config repo. The first Pages deployment requires the workflow to run at least once — either push a commit to the default branch or trigger it manually from the Actions tab. The CLI prints the expected Pages URL (https://<org>.github.io/classroom50/) after init finishes; it may take a minute to go live.
If init warns that the org-level workflow token policy is too restrictive (the endpoint is org-scoped and requires an org owner to change it), you can apply it yourself:
gh api -X PUT /orgs/<org>/actions/permissions/workflow \
-f default_workflow_permissions=write \
-f can_approve_pull_request_reviews=falseSimilarly, if the reusable workflow access warning fires:
gh api -X PUT /repos/<org>/classroom50/actions/permissions/access \
-f access_level=organizationScores are collected by the collect-scores.yaml Actions workflow in your classroom50 config repo. It runs on a nightly cron (17 4 * * * UTC) automatically once init is done. To trigger it manually:
gh workflow run collect-scores.yaml --repo <org>/classroom50
gh workflow run collect-scores.yaml --repo <org>/classroom50 -f classroom=<short-name> # single classroomOr use the Actions tab on <org>/classroom50 → collect-scores.yaml → Run workflow.
The CLIs call the GitHub REST API through go-gh (RESTClient), which resolves paths relative to https://api.github.com on github.com or https://<host>/api/v3 on GitHub Enterprise Server. The collect_scores.py script uses Python's urllib with a Bearer token. No Octokit, no axios, no raw fetch().
| Method | URL | Purpose |
|---|---|---|
| GET | https://api.github.com/user |
Verify authenticated identity (whoami) |
| GET | https://api.github.com/orgs/{org} |
Check org plan (warn if Pages from a private repo requires Team or Enterprise Cloud) |
| PATCH | https://api.github.com/orgs/{org} |
Lock down org member privileges to least-privilege at init time. The only enabled member capabilities are private repo creation (members_can_create_private_repositories: true) and public Pages creation (members_can_create_pages: true + members_can_create_public_pages: true, enforced so the config repo's public Pages site can publish — re-running init resets it). Everything else is denied: default_repository_permission: "none", and false for public/internal repo creation, private Pages, repo delete/transfer, repo visibility change, issue deletion, team creation, dependency-insights viewing, private-repo forking, member-invited outside collaborators, and read-access discussion creation. Combined PATCH with a per-field retry on 403/422 so a plan-gated field only warns. |
| GET | https://api.github.com/orgs/{org}/actions/permissions |
Read whether GitHub Actions is enabled for the org |
| PUT | https://api.github.com/orgs/{org}/actions/permissions |
Enable GitHub Actions for the org when it's disabled org-wide |
| POST | https://api.github.com/orgs/{org}/repos |
Create the classroom50 config repo |
| GET | https://api.github.com/repos/{owner}/{repo} |
Check whether the config repo already exists |
| POST | https://api.github.com/repos/{owner}/{repo}/pages |
Enable GitHub Pages (workflow build source) |
| PUT | https://api.github.com/repos/{owner}/{repo}/pages |
Set Pages visibility to public |
| PUT | https://api.github.com/repos/{owner}/{repo}/branches/{branch}/protection |
Branch protection on the config repo (no force-push, no delete) |
| GET | https://api.github.com/repos/{owner}/{repo}/actions/permissions |
Read whether Actions is enabled for the config repo |
| PUT | https://api.github.com/repos/{owner}/{repo}/actions/permissions |
Re-enable Actions on the config repo when it's off |
| GET | https://api.github.com/repos/{owner}/{repo}/actions/permissions/workflow |
Read current workflow token policy (detect org-enforced override) |
| PUT | https://api.github.com/repos/{owner}/{repo}/actions/permissions/workflow |
Set default GITHUB_TOKEN to write permissions |
| PUT | https://api.github.com/repos/{owner}/{repo}/actions/permissions/access |
Allow reusable workflows from the same org |
| GET | https://api.github.com/repos/{owner}/{repo}/actions/secrets/public-key |
Retrieve sealbox public key for encrypting the collect PAT |
| PUT | https://api.github.com/repos/{owner}/{repo}/actions/secrets/CLASSROOM50_COLLECT_TOKEN |
Upload the encrypted collect PAT as an Actions secret |
| GET | https://api.github.com/repos/{owner}/{repo}/contents/{path} |
Read existing files in the config repo (idempotency checks, skeleton probing) |
| GET | https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch} |
Resolve branch tip SHA before a tree commit |
| GET | https://api.github.com/repos/{owner}/{repo}/git/commits/{commit_sha} |
Read parent commit metadata |
| POST | https://api.github.com/repos/{owner}/{repo}/git/blobs |
Upload file content as a blob (for tree commits) |
| POST | https://api.github.com/repos/{owner}/{repo}/git/trees |
Create a new git tree |
| POST | https://api.github.com/repos/{owner}/{repo}/git/commits |
Create a commit object |
| PATCH | https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch} |
Fast-forward the branch to the new commit (with rebase retry on conflict) |
| GET | https://api.github.com/repos/{owner}/{repo} |
Validate a template repo and read its default branch |
| GET | https://api.github.com/users/{username} |
Resolve a GitHub login to its numeric account ID |
| POST | https://api.github.com/orgs/{org}/invitations |
Send an org membership invitation |
| GET | https://api.github.com/orgs/{org}/memberships/{username} |
Check a user's org membership state |
| DELETE | https://api.github.com/orgs/{org}/memberships/{username} |
Remove a user from the org |
| PUT | https://api.github.com/repos/{owner}/{repo}/collaborators/{username} |
Add a repo collaborator (direct invite) |
| DELETE | https://api.github.com/repos/{owner}/{repo}/collaborators/{username} |
Remove a repo collaborator |
| DELETE | https://api.github.com/repos/{owner}/{repo} |
Delete a repository (gh teacher teardown; requires the delete_repo OAuth scope, not granted by default — opt in with gh teacher login -s delete_repo) |
| GET | https://api.github.com/repos/{owner}/{repo}/releases/latest |
Fetch the latest release for score collection or download |
| GET | https://api.github.com/repos/{owner}/{repo}/releases |
List recent releases (fallback when latest isn't a submit/* tag) |
| GET | https://api.github.com/orgs/{org}/repos |
Page through org repos for --by-pattern download mode |
| GET | https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id} |
Download a result.json release asset (Accept: application/octet-stream) |
| GET | https://api.github.com/classrooms |
List GitHub Classroom classrooms the user administers (org-name --source resolution for migrate) |
| GET | https://api.github.com/classrooms/{classroom_id} |
Get a single GitHub Classroom by ID (numeric --source for migrate) |
| GET | https://api.github.com/classrooms/{classroom_id}/assignments |
List a source classroom's assignments (migrate discovery) |
| GET | https://api.github.com/assignments/{assignment_id} |
Fetch a source assignment's starter_code_repository + deadline (migrate discovery) |
| POST | https://api.github.com/repos/{template_owner}/{template_repo}/generate |
Copy each source starter repo into the target org as a fresh template (migrate template copy) |
| PATCH | https://api.github.com/repos/{owner}/{repo} |
Set is_template: true on the newly-generated target repo (migrate template copy) |
| GET | https://api.github.com/repos/{owner}/{repo}/branches/{branch} |
Wait for the freshly-generated target repo's branch ref to stabilize (migrate template copy) |
| Method | URL | Purpose |
|---|---|---|
| GET | https://api.github.com/user |
Verify authenticated identity (whoami, git identity) |
| GET | https://api.github.com/user/memberships/orgs/{org} |
Check whether the student is already an org member |
| PATCH | https://api.github.com/user/memberships/orgs/{org} |
Accept a pending org invitation |
| POST | https://api.github.com/repos/{template_owner}/{template_repo}/generate |
Generate the student's assignment repo from the template |
| GET | https://api.github.com/repos/{owner}/{repo} |
Recover from a 422 "repository already exists" during generate |
| PATCH | https://api.github.com/repos/{owner}/{repo} |
Disable issues, projects, and wiki on the new repo (visibility is set to private at generation time) |
| PUT | https://api.github.com/repos/{owner}/{repo}/collaborators/{username} |
Keep the student as an admin collaborator on their assignment repo (org-level member-privilege lockdown defangs the org-wide danger of repo-admin) |
| GET | https://api.github.com/repos/{owner}/{repo}/branches/{branch} |
Wait for the template's default branch to stabilize after generate |
| GET | https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch} |
Resolve branch tip SHA before a tree commit |
| GET | https://api.github.com/repos/{owner}/{repo}/git/commits/{commit_sha} |
Read parent commit metadata |
| POST | https://api.github.com/repos/{owner}/{repo}/git/blobs |
Upload file content as blobs (used by both accept and submit) |
| POST | https://api.github.com/repos/{owner}/{repo}/git/trees |
Create a new git tree |
| POST | https://api.github.com/repos/{owner}/{repo}/git/commits |
Create a commit object |
| PATCH | https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch} |
Fast-forward the branch to the new commit |
| GET | https://api.github.com/repos/{owner}/{repo}/contents/{path} |
Fetch .gitignore and .github/ from the template repo (gh student submit only) |
| Method | URL | Purpose |
|---|---|---|
| GET | https://api.github.com/repos/{owner}/{repo}/releases/latest |
Fetch the latest release for a student's assignment repo |
| GET | https://api.github.com/repos/{owner}/{repo}/releases |
List recent releases (fallback when latest isn't a submit/* tag) |
| GET | https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id} |
Download the result.json asset from a release |
These calls happen inside the autograde workflow on every student submission. GH_TOKEN is set to ${{ github.token }} (the student repo's GITHUB_TOKEN).
| Method | URL | Purpose |
|---|---|---|
| POST | https://api.github.com/repos/{owner}/{repo}/statuses/{sha} |
Post a classroom50/autograde commit status (pending → success/failure) |
The workflow also uses git tag + git push against the student's repo to create the submit/<UTC-timestamp>-<short-sha> tag for branch-triggered runs, and gh release subcommands (view, edit, upload, create) to publish the result.json asset on the submission tag release. The latest-pointer flip lives in a separate set-latest job with a per-repo concurrency group so concurrent submissions can't race on the read-modify-write.
The runner setup also fetches from GitHub Pages without authentication (public by design):
| Method | URL | Purpose |
|---|---|---|
| GET | https://{org}.github.io/classroom50/{classroom}/assignments.json |
Load the classroom's assignment manifest (and the per-assignment runtime block for the grade job) |
| GET | https://{org}.github.io/classroom50/runner.py |
Fetch the runner-side bootstrap (org-level; one per config repo, shared across all classrooms) |
| GET | https://{org}.github.io/classroom50/{classroom}/autograder.py |
Fetch the classroom default autograder (only when set via gh teacher autograder set-default and the assignment has no per-assignment override). 404 → runner publishes a vacuous-pass result. |
| GET | https://{org}.github.io/classroom50/{classroom}/autograders/{slug}.tar.gz |
Download the per-assignment bundle (entrypoint autograder.py plus any sibling fixtures; fetched by runner.py at runtime) |
| File | Triggers | Purpose |
|---|---|---|
publish-pages.yaml |
Push to default branch, workflow_dispatch
|
Deploy assignments.json, classroom-default autograder.py files, autograder workflow shims, the runner-side bootstrap, and per-assignment bundles to GitHub Pages |
collect-scores.yaml |
workflow_dispatch, cron 17 4 * * * UTC |
Run collect_scores.py, aggregate result.json assets into */scores.json
|
autograde-runner.yaml (reusable) |
Called from each student's autograde.yaml
|
Set up runtime (toolchains, container) per assignments.json, fetch runner.py from Pages, run it, publish commit status and submit-tag release |
| Variable / Secret | Where set | Used by | Purpose |
|---|---|---|---|
CLASSROOM50_COLLECT_TOKEN |
gh teacher init (Actions secret on classroom50) |
collect-scores.yaml, collect_scores.py
|
Fine-grained PAT for reading student repo releases |
GITHUB_TOKEN / github.token
|
Automatically injected by Actions | autograde-runner.yaml |
Student-repo Actions token (contents: write, statuses: write) |
GH_TOKEN |
Set from github.token in runner steps |
gh api, gh release inside the runner |
gh CLI auth inside the autograde workflow |
GH_DEBUG=api |
Developer shell |
go-gh (teacher & student CLIs) |
Log all REST request/response traffic |
GITHUB_REPOSITORY_OWNER |
Actions context | collect_scores.py |
Org name inside the collect workflow |
GITHUB_API_URL |
Actions context | collect_scores.py |
API base URL (supports GitHub Enterprise Server) |
GH_API_URL |
Test override |
collect_scores.py tests |
Override API base in unit tests |
The teacher and student CLIs do not use GITHUB_TOKEN or GH_TOKEN. They read credentials from the gh auth store (typically ~/.config/gh/hosts.yml), populated by gh teacher login / gh student login.