-
Notifications
You must be signed in to change notification settings - Fork 26
GitHub Integration
Every place Classroom 50 touches GitHub: what you do manually, what the CLI handles, and the complete list of REST API calls the tooling makes.
The CLI never creates the organization. Before running any CLI command:
-
Create the organization at https://github.com/account/organizations/new.
Free orgs work for public templates; Team or Enterprise Cloud is required for
Pages from the private
classroom50repository. - Flag your template repositories under Settings → General → Template repository.
Note
A private template must live inside your org; see Template visibility.
gh teacher init locks organization member privileges to least-privilege
automatically. After it runs, a member can only create a private repository
(so gh student accept works) and publish a public Pages site (so the
classroom50 repository's assignments.json stays reachable).
Why broad access to their own repository is safe for students
The lockdown denies the dangerous org-wide powers (private Pages, repo
delete/transfer, visibility change, issue deletion, team creation, dependency
insights, member-invited outside collaborators). Public-repo creation is the one
exception by plan: it's locked off only on Enterprise Cloud, because Team/Free
couples public and private creation and the student flow needs private creation.
So it's safe for gh student accept to grant broad access to a student's own
repository — individual students are downgraded to write after creation, a
group founder keeps admin to add teammates, and the org locks defang the
rest.
Four member-privilege settings have no API, so init can't set them and
audit can't read them. Apply them once by hand; the checklist is in
Manual organization hardening
in the CLI Teacher Guide.
Run once per machine, or after a token rotation:
gh teacher loginThis wraps gh auth login -s admin:org -s read:org -s repo -s workflow — the
unified Classroom 50 scope set, shared with gh student login, so
authenticating one CLI covers the other. (delete_repo is not included — opt
in with gh teacher login -s delete_repo for teardown.)
You often don't need to run it. Every other command checks your existing gh
credentials first: a sufficiently-scoped token is reused untouched, and an
under-scoped token that gh manages is widened in place with gh auth refresh
(your token is kept). login itself always re-runs gh auth login, which
replaces your stored github.com token — so when one already exists, the CLI
warns and asks for confirmation before proceeding. See
Will gh teacher login disturb my existing gh setup?.
| Scope | Required for |
|---|---|
admin:org |
Org invitations, reading and removing memberships (implies read:org). |
read:org |
Checking org membership. |
repo |
Repo creation, contents writes, collaborators. |
workflow |
Committing the classroom50 repository's workflow files during init (GitHub 404s the write without it). |
Note
Tearing down an organization needs an extra permission. Signing in does
not request delete_repo. Exactly one feature needs it: Tear down
organization, in the organization's settings under Danger zone. It
resets an organization by deleting every repository in it, not only the
ones Classroom 50 created. When you use it, Classroom 50 asks you to request
that permission and sign in again, and teardown still runs only after you type
an explicit confirmation. Nothing else deletes repositories. Classroom 50 has
no server: the token stays in your browser, so nobody but you can act on your
organization with it. The CLIs work the same way: they never request
delete_repo unless you opt in with gh teacher login -s delete_repo.
If you signed in with a personal access token, Classroom 50 can't add the
permission for you — a token's permissions are fixed when you create it on
GitHub. Create one that allows deleting repositories (the delete_repo scope
on a classic token, or Administration: read and write on a fine-grained
one) and sign in again.
gh student loginSame device flow and the same scope set, so a student who authenticated a
teacher CLI (or vice versa) needs no re-auth. A student exercises read:org
(accept org membership), repo (generate repos, collaborators), and workflow
(commit the autograde shim at accept).
gh teacher init uploads a PAT into the CLASSROOM50_SERVICE_TOKEN secret; the
score-collection, regrade, and token-probe workflows (collect-scores.yaml,
regrade.yaml, probe-token.yaml) use it to read student repos across the org.
Create it at https://github.com/settings/personal-access-tokens/new from your own account (scope it tightly to the org):
| Setting | Value |
|---|---|
| Resource owner | Your teaching org. |
| Repository access | All repositories ("Only select repositories" misses on-demand student repos). |
| Contents |
Read and write (read: collect; write: regrade pushes submit/* tags). |
| Actions | Read and write (regrade re-runs autograde). |
| Administration | Read and write (grant staff teams read on student repos/templates). |
| Metadata | Read (auto-included; lets collection read group-repo collaborators). |
| Organization → Members | Read (list the classroom team; separate section, shown only once the org is the resource owner). |
| Expiry | Up to 1 year; set a rotation reminder. |
Important
Members: Read is under Organization permissions, not Repository permissions, and isn't implied by any repository scope. A Contents-only token passes a Contents check but fails the first call collection makes.
Note
Group assignments need no extra scope. Collection reads a group repo's
collaborators via Metadata: read (auto-included) and credits members on the
classroom team. If the read fails, the owner is still scored and a warning is
logged.
Supply the token via the environment variable (never a flag — command-line PATs leak via shell history):
CLASSROOM50_SERVICE_TOKEN=github_pat_... gh teacher init <org>It's encrypted with libsodium before upload and never written to disk. Rotate
with gh teacher rotate-service-token <org>.
init enables Pages and sets visibility to public. The first deployment needs
the publish-pages.yaml workflow to run once — push to the default branch or
trigger it from the Actions tab. The CLI prints the Pages URL
(https://<org>.github.io/classroom50/) after init.
If the organization's Pages site uses a custom domain, GitHub answers
github.io requests with a redirect that students' browsers reject. Set the
classroom's Custom Pages domain in the web app so browsers fetch from the
custom domain directly; server-side readers (the CLIs and Actions workflows)
follow the redirect and are unaffected. See
Using a custom Pages domain.
If init warns that the org workflow-token policy or reusable-workflow access is
too restrictive, apply them yourself:
gh api -X PUT /orgs/<org>/actions/permissions/workflow \
-f default_workflow_permissions=write -f can_approve_pull_request_reviews=false
gh api -X PUT /repos/<org>/classroom50/actions/permissions/access \
-f access_level=organizationTrigger the collect-scores.yaml workflow from the Actions tab, optionally
scoped to one classroom or a single assignment (the same scoped runs the web
app dispatches: Collect all on a classroom's assignments list sends the
classroom-only scope, and the per-assignment Collect now sends both inputs):
gh workflow run collect-scores.yaml --repo <org>/classroom50
gh workflow run collect-scores.yaml --repo <org>/classroom50 -f classroom=<short-name>
gh workflow run collect-scores.yaml --repo <org>/classroom50 -f classroom=<short-name> -f assignment=<slug>A scoped run walks only the matching repos — one classroom's, or a single
assignment's (faster and cheaper on API rate limits for a large classroom) —
and stamps each walked assignment's
collected_at in scores.json; buckets outside the scope are untouched.
The staff-team read grant that rides along with collection is scoped the same
way, so a per-assignment run touches only that assignment's repositories and
template.
After init/rotate, or when collect/regrade returns 401/403, run the
read-only probe:
gh workflow run probe-token.yaml --repo <org>/classroom50A green run confirms every scope; a red run's log names the missing scope(s). Side-effect free.
Classroom 50 has no server. Whatever you authorize is a GitHub token that lives
only in your browser (web app) or your local gh credential store (CLI) —
nobody but you can act on your account with it. Sign-in requests one scope set
for everyone — a deliberate simplicity choice, not a technical requirement.
Teachers and students share a single flow because one person can be both (a
teacher testing an assignment as a student, a TA who also takes the course), so
the app asks for the union of what any role might need rather than making you
declare a role up front. A student's grant is therefore broader than what a
student actually uses. Capabilities are gated after sign-in by your role in the
organization and classroom, not by the scopes on your token.
The scopes below are GitHub's own. The table lists what each one grants across your whole account, why Classroom 50 needs it, and who actually exercises it.
| Scope | Access it grants | Why Classroom 50 needs it | Who uses it |
|---|---|---|---|
read:user |
Reads your public profile. | Identifies who you are after sign-in. | Everyone. |
read:org |
Reads your organization and team memberships. | Confirms membership and resolves your classroom role; a student accepts their own org invitation. | Everyone. |
repo |
Full control of all your repositories — public and private, in every organization, not only the classroom one. GitHub offers no way to narrow it to a single org. | Creating student repositories, committing config and setup files, reading grades (private-repo Releases), and managing repo collaborators. | Everyone. |
workflow |
Commit files under .github/workflows/ in repositories you can write. |
Landing the autograder workflow — teachers during init, students when the browser commits the autograde shim on accept. |
Everyone (students only for default-autograder accepts). |
admin:org |
Administer organizations you own — invite/remove members, manage teams, change org settings. | Inviting and removing students, managing classroom teams, and locking down org policy. Implies read:org. |
Teachers only. |
delete_repo |
Permanently delete repositories. | Not requested at sign-in. One feature needs it — Tear down organization — and Classroom 50 asks for it on demand, then only after an explicit typed confirmation (see the teacher-authentication note above). | Teachers only, on demand. |
A student's flow only ever exercises three scopes: read:user (identify
themselves), read:org (accept their own organization invitation and read
their own memberships), and repo (generate their assignment repository, commit
their work, add a teammate as a collaborator on a group repository, and read
their own grades). Default-autograder assignments also need workflow, because
the browser commits the autograde workflow file on accept; template-less and
no-autograder assignments don't. That's the whole student footprint.
A student never uses admin:org or delete_repo. Those are organization-owner
powers — a plain member's token can't perform them on an org they don't own,
even though the shared grant nominally includes admin:org. The app requests
them because the sign-in flow is shared with teachers, not because a student
needs them. So the grant is broader than the footprint: the token can touch
all your repositories, but Classroom 50 only ever acts on classroom ones. This
matches the GitHub CLI's behavior, where gh teacher login and gh student login share one scope set for the same reason.
The one lever that actually narrows the grant is a fine-grained personal
access token, which is scoped to a single organization instead of your whole
account. On the sign-in card, open Other sign-in methods and pick Use a
personal access token (fine-grained); you name the org (it becomes the token's
resource owner) and set Repository access to All repositories. See
If the proxy domain is blocked for the full
walkthrough. A classic OAuth sign-in can't be scoped this way — the repo scope
is all-or-nothing — so the fine-grained token is the tighter-security path for
anyone who wants to grant less. Why classic OAuth can't be scoped per
organization, and why sign-in doesn't ask you to pick a teacher or student role,
are recorded in Known Limitations.
If your school or district filters web traffic, allow the domains below so Classroom 50 works end to end. The web app runs entirely in the browser, so the browser itself must reach these hosts.
| Domain | Used by | For |
|---|---|---|
classroom50.org, preview.classroom50.org
|
Web app | Loading the app. |
classroom50.fifty-foundation.workers.dev |
Web app | The GitHub proxy (OAuth sign-in and repo downloads). See The GitHub proxy below. |
github.com |
Web app, CLI | OAuth sign-in and CLI authentication. |
api.github.com |
Web app, CLI, Actions | All GitHub REST API calls (classrooms, rosters, assignments, grading). |
*.github.io |
Web app, Actions | The org's Pages config (<org>.github.io/classroom50/…): the assignment manifest, autograders, and the runner. If the org's Pages site uses a custom domain, allow that domain too — see Using a custom Pages domain. |
codeload.github.com |
Web app | Repo archive (zip) downloads, reached through the proxy. |
www.githubstatus.com |
Web app | GitHub status check for the outage banner (best-effort). |
A browser can't do everything GitHub requires directly, so the web app sends two
operations through a small proxy. It defaults to the Fifty Foundation Cloudflare
Worker at classroom50.fifty-foundation.workers.dev:
- OAuth sign-in. Exchanging the login code for an access token needs the OAuth client secret, which can't be shipped in browser JavaScript. The proxy holds the secret and performs the exchange.
-
Repo downloads. GitHub's archive endpoint redirects to
codeload.github.com, which doesn't send the CORS headers a browser needs to follow the redirect. The proxy follows it server-side and streams the zip back.
The proxy is configurable: set VITE_GITHUB_PROXY_BASE at build time to point
the app at your own proxy instead of the default worker. Everything else the web
app does talks to api.github.com directly and doesn't involve the proxy.
Some networks can't allow workers.dev. When the proxy is unreachable:
-
What still works: everything except the two operations above. All other
GitHub calls go straight to
api.github.com, so browsing classrooms, rosters, and assignments is unaffected. - What breaks: the normal "Sign in with GitHub" button (its token exchange goes through the proxy) and in-app repo downloads.
-
Signing in anyway: paste a personal access token instead. On the sign-in
card, open Other sign-in methods and pick Use a personal access token
(classic) or Use a personal access token (fine-grained). Both validate
the token directly against
api.github.com, so they never touch the proxy, and both link to a token-creation page with the required scopes/permissions pre-filled. A fine-grained token works with one organization only — you name the org (it becomes the token's resource owner) and must set Repository access to All repositories. (This sign-in token is one you paste into the web app; it is separate from the fine-grainedCLASSROOM50_SERVICE_TOKENused for score collection.) -
A fuller fix: host the proxy yourself on a domain your network already
allows and point
VITE_GITHUB_PROXY_BASEat it. This restores both the normal sign-in and repo downloads.
The CLIs call GitHub through go-gh;
collect_scores.py uses urllib with a bearer token.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /user |
Whoami. |
| GET | /orgs/{org} |
Check org plan. |
| PATCH | /orgs/{org} |
Lock down member privileges at init. |
| GET / PUT | /orgs/{org}/actions/permissions |
Read/enable org Actions. |
| POST | /orgs/{org}/repos |
Create the classroom50 repository. |
| GET | /repos/{owner}/{repo} |
Check the classroom50 repository / validate a template. |
| POST / PUT | /repos/{owner}/{repo}/pages |
Enable Pages and set it public. |
| PUT | /repos/{owner}/{repo}/branches/{branch}/protection |
Protect the classroom50 repository branch. |
| GET / PUT | /repos/{owner}/{repo}/actions/permissions |
Read/re-enable Actions on the classroom50 repository. |
| GET / PUT | /repos/{owner}/{repo}/actions/permissions/workflow |
Read/set GITHUB_TOKEN permissions. |
| PUT | /repos/{owner}/{repo}/actions/permissions/access |
Allow same-org reusable workflows. |
| GET / PUT | /repos/{owner}/{repo}/actions/secrets/... |
Upload the encrypted service PAT. |
| GET / POST / PATCH | /repos/{owner}/{repo}/git/{refs,commits,blobs,trees} |
Tree-commit config files (with rebase retry). |
| GET | /users/{username} |
Resolve a login to its numeric ID. |
| POST | /orgs/{org}/invitations |
Send an org invitation. |
| GET / DELETE | /orgs/{org}/memberships/{username} |
Check / remove org membership. |
| PUT / DELETE | /repos/{owner}/{repo}/collaborators/{username} |
Add / remove a repo collaborator. |
| DELETE | /repos/{owner}/{repo} |
Delete a repo (teardown; needs delete_repo). |
| GET |
/repos/{owner}/{repo}/releases + /releases/assets/{id}
|
Collect submit/* releases and result.json. |
| GET | /orgs/{org}/repos |
Page org repos for --by-pattern download. |
| GET |
/classrooms, /classrooms/{id}, /classrooms/{id}/assignments, /assignments/{id}
|
GitHub Classroom discovery (migrate). |
| POST / PATCH |
/repos/{owner}/{repo}/generate, /repos/{owner}/{repo}
|
Copy source starter repos as templates (migrate). |
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /user |
Whoami / git identity. |
| GET / PATCH | /user/memberships/orgs/{org} |
Check / accept a pending org invite. |
| POST | /repos/{template_owner}/{template_repo}/generate |
Generate the repo from a template. |
| POST | /orgs/{org}/repos |
Create the repo directly (template-less accept). |
| GET / PATCH | /repos/{owner}/{repo} |
Recover from "already exists"; read the template's features and apply the assignment's repository-feature settings (inherit/on/off). |
| PUT | /repos/{owner}/{repo}/collaborators/{username} |
Set the founder role: push (individual) or admin (group); also backs gh student invite. |
| GET / POST / PATCH |
/repos/{owner}/{repo}/git/{refs,commits,blobs,trees} + /branches/{branch}
|
Commit the setup files. |
| GET | /repos/{owner}/{repo}/contents/{path} |
Fetch .gitignore/.github/ from the template (submit). |
| Method | Endpoint | Purpose | FG-PAT permission |
|---|---|---|---|
| GET | /orgs/{org}/teams/{slug}/members |
List the classroom team (team-driven enrollment). | Members: Read |
| GET |
/repos/{owner}/{repo}/releases + /releases/assets/{id}
|
Collect submissions and result.json. |
Contents: Read |
| GET | /repos/{owner}/{repo}/collaborators |
Fan a group score to teammates. | Metadata: Read |
| GET / PUT | /orgs/{org}/teams/{slug}/repos/{owner}/{repo} |
Grant staff teams read on student repos/templates. | Administration: R/W |
Exercises every scope with read-only proxies GitHub gates behind the write
permission: /orgs/{org}/members, /orgs/{org}/teams/{slug}/members,
/repos/{org}/classroom50 (its permissions.push/admin),
/repos/{org}/classroom50/actions/permissions, and
/repos/{org}/classroom50/collaborators.
Jobs: setup (create the submit tag, validate config), grade (run
runner.py + the autograder, post status, publish the Release, maintain the
Feedback PR), and set-latest (serialized latest-pointer update). It posts
/repos/{owner}/{repo}/statuses/{sha}, uses git tag/git push and gh release for tags and Releases, and fetches unauthenticated from Pages:
| Endpoint | Purpose |
|---|---|
https://{org}.github.io/classroom50/{classroom}/assignments.json |
The assignment manifest + runtime block. |
https://{org}.github.io/classroom50/runner.py |
The runner bootstrap (org-level). |
https://{org}.github.io/classroom50/{classroom}/autograder.py |
The classroom default (404 → vacuous pass). |
https://{org}.github.io/classroom50/{classroom}/autograders/{slug}.tar.gz |
The per-assignment bundle. |
| File | Triggers | Purpose |
|---|---|---|
publish-pages.yaml |
Push to default branch, workflow_dispatch
|
Deploy assignments.json, autograders, shims, runner.py, and bundles to Pages. |
collect-scores.yaml |
workflow_dispatch |
Aggregate result.json into */scores.json. |
regrade.yaml |
workflow_dispatch |
Push regrade tags to student repos for an assignment. |
probe-token.yaml |
workflow_dispatch |
Read-only service-token scope check. |
autograde-runner.yaml (reusable) |
Called by each student's autograde.yaml
|
Grade, publish, update the latest pointer. |
| Variable / Secret | Set by | Used by | Purpose |
|---|---|---|---|
CLASSROOM50_SERVICE_TOKEN |
gh teacher init |
collect-scores.yaml, regrade.yaml, probe-token.yaml
|
Read student repo releases; regrade; probe the token. |
GITHUB_TOKEN |
Actions | Runner jobs | Tags, status, Release, Feedback PR. |
GH_DEBUG=api |
Developer | go-gh |
Log REST traffic. |
GITHUB_REPOSITORY_OWNER / GITHUB_API_URL
|
Actions | collect_scores.py |
Org name and API base (supports Enterprise Server). |
The teacher and student CLIs read credentials from the gh auth store (populated
by gh teacher login / gh student login), not from GITHUB_TOKEN.
- Start here
- Teacher guides
- Autograding
- Students
- Reference