Skip to content

GitHub Integration

classroom50-sync[bot] edited this page Jun 18, 2026 · 13 revisions

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.

Manual steps

1. Org setup (one-time, github.com web UI)

The CLI never creates the org for you. Do the following once before running any CLI commands:

  1. 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 classroom50 config repo is private).

  2. 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-history org 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.)

2. Teacher authentication (gh teacher login)

Run once per machine or after a token rotation:

gh teacher login

This 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)

3. Student authentication (gh student login)

Run once per student machine:

gh student login

Same 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

4. Fine-grained PAT for score collection

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.py then 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 only Metadata: read, which is auto-included on every fine-grained PAT and already implied by the Contents: read grant 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 and collect-scores logs 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>

5. GitHub Pages (automatic, but requires Actions to run)

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=false

Similarly, if the reusable workflow access warning fires:

gh api -X PUT /repos/<org>/classroom50/actions/permissions/access \
  -f access_level=organization

6. Score collection

Scores 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 classroom

Or use the Actions tab on <org>/classroom50collect-scores.yamlRun workflow.


GitHub REST API reference

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().

gh teacher CLI

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)

gh student CLI

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)

collect_scores.py (runs inside GitHub Actions, uses CLASSROOM50_COLLECT_TOKEN)

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

autograde-runner.yaml (reusable workflow, runs in student repos)

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)

GitHub Actions workflows

Scaffolded by gh teacher init into each <org>/classroom50 config repo

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

Environment variables and secrets summary

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.

Clone this wiki locally