Configuration repo for a Classroom 50 teaching organization.
This repo holds:
- Per-classroom directories (created by
gh teacher classroom add):classroom.json— name, term, org (public)assignments.json— assignment manifest with optional per-entryruntimeblock (semi-public; published via GitHub Pages)students.csv— roster (private). Required columns:username,first_name,last_name,email,section,github_id. Theemailcolumn is optional per row;github_idis CLI-managed (populated bygh teacher roster add/importfromGET /users/{username}) and should not be hand-edited. Any extra trailing columns aftergithub_id(e.g. left over from an older file) are read past and preserved on edit, so don't remove them by hand.scores.json— collected submission scores (private)autograder.py(optional, classroom default) — set viagh teacher autograder set-default. Used by every assignment in the classroom that doesn't have its own override. Absent by default — submissions still tag and publish releases (vacuous-passresult.json) when no autograder is configured.autograders/<slug>/(optional, per-assignment) — entrypointautograder.pyplus any sibling fixtures, helpers, or framework configs. Bundled as<slug>.tar.gzbypublish-pages.yamland downloaded by the runner-side bootstrap at workflow runtime. Takes precedence over the classroom default.autograders/<name>.yaml(optional, rare) — non-default autograder shim referenced viagh teacher assignment add --autograder <name>. Used when an assignment needs a different reusable workflow entirely; for different language toolchains or apt packages, edit theruntime:block on the assignment instead.
.github/scripts/(org-level, shared across all classrooms in this repo; nested under.github/so the directory name can't collide with a classroom slug):runner.py— runner-side bootstrap fetched from Pages on every submission. Downloads the per-assignment bundle, resolves the entrypoint (per-assignment override → classroom default<classroom>/autograder.py→ vacuous-pass synthesis), execs it withcwdat the student's checkout, validates the v1result.jsonit produces. Teachers don't normally edit this file.materialize_tests.py— expands an assignment's declarativetestsblock (fromassignments.json) into the per-assignment bundle so the runner can gradeio/run/pythonspecs without a hand-writtenautograder.py.ensure_feedback_pr.py— maintains the long-lived per-repo Feedback pull request (opt-in per assignment); invoked fromautograde-runner.yamlafter grading.collect_scores.py-- score collector. Roster-driven: walks every(student, assignment)pair from<classroom>/students.csvx<classroom>/assignments.jsonand pages through that pair's<classroom>-<assignment>-<username>repo'ssubmit/*releases. Each release carries aresult.jsonasset (produced by the autograder); the collector schema-validates it, checks the embedded(classroom, assignment, username)triple against the source repo's expected identity, and upserts it into<classroom>/scores.json. The gradebook root is anassignmentsmap keyed by assignment slug; each value is{type, entries}, and each entry holds asubmissionshistory list (the now-redundantassignmentfield is dropped from each stored row). Legacy layouts (a flat array, or an oldersubmissions-keyed root) are not migrated — the collector hard-fails on them by design. Honors"override": trueso teacher manual corrections never get overwritten. Per-classroom writes are atomic (scores.json.tmp->os.replace). A 404 from any expected repo's releases endpoint is not an error -- it just means the student hasn't accepted or submitted yet; the collector logs a per-assignment "X of Y submitted" summary so teachers see roster coverage at a glance.regrade_repos.py-- regrade fan-out. Roster-driven like the collector, but scoped to a single classroom + assignment (and optionally a single repo owner). For each targeted<classroom>-<assignment>-<username>repo it re-runs the repo's latestautograde.yamlworkflow run via the Actions rerun API — grading the same commit again and re-fetching the current autograder from Pages, so a teacher's fixed test / updated autograder takes effect without changing the student's submission time orlateflag (the runner stampsdatetimefrom the graded commit's committer date). A repo that has amainHEAD but no prior autograde run is first-graded by the fallback: pushing a freshsubmit/<UTC-timestamp>-<short-sha>tag, which fires its autograde workflow. Idempotent: asubmit/*tag already pointing at HEAD is reused, not duplicated. Repos with nomainHEAD (never accepted) are skipped. Re-run/tag only — grading then runs asynchronously inside each student repo, and the resulting releases are picked up by the nextcollect_scores.pyrun. Per-repo 404 (not accepted) / 422 (tag race) / non-rerunnable run warn and skip; an auth/network failure aborts the run.
.github/workflows/:autograde-runner.yaml— reusable workflow called by every student-repo autograde shim. Reads.classroom50.yaml+ the assignment'sruntimeblock, decides the runner OS / container / language toolchains, fetchesrunner.pyfrom Pages (published from.github/scripts/runner.py), runs it, posts the commit status, and publishes the submit-tag release.publish-pages.yaml— builds the Pages site from an allow-list of paths (runner.pyat the site root, per-classroom JSON, per-classroomautograder.pydefaults, autograder shim YAMLs, per-assignment bundles).collect-scores.yaml— teacher-triggered (manual viaworkflow_dispatch, nightly via cron). Calls.github/scripts/collect_scores.py, then commits any updated*/scores.jsonfiles back to the repo.regrade.yaml— teacher-triggered (manual viaworkflow_dispatch, dispatched by the web app's "Regrade" actions). Inputs:classroom(required),assignment(required),owner(optional single repo). Calls.github/scripts/regrade_repos.pyto re-run the targeted student repos' latest autograde runs (tagging only as a first-grade fallback) so the autograder re-runs. Does not collect scores itself (grading runs asynchronously); the new releases are ingested by the nextcollect-scores.yamlrun.
Bootstrapped by gh teacher init <org>. From there:
gh teacher classroom add <org> <short-name>— scaffold a new classroom directory (the four config files above; the per-classroomautograder.pyand per-assignmentautograders/<slug>/folders fill in over time as needed).gh teacher roster add|remove|import <org> <classroom> ...— managestudents.csv(and auto-invite new students to the org).gh teacher assignment add|remove <org> <classroom> <slug>— register or drop an assignment inassignments.json. Pass--runtime <path>for per-assignment runtime customization (Python version, language toolchains, apt packages, container image). Pass--autograder <name>only when swapping the entire reusable workflow (rare). Per-assignment grading is NOT registered through this command — drop anautograder.py(plus any sibling fixtures) at<classroom>/autograders/<slug>/, or rungh teacher autograder set-default <org> <classroom>to install a classroom default.gh teacher autograder set-default <org> <classroom>— install a classroom defaultautograder.pyat<classroom>/autograder.py. With--from <path>, uploads the given Python source. Without--from, drops a diagnostic stub for verifying the runner pipeline.gh teacher assignment list <org> <classroom>— print every assignment slug registered in a classroom (--jsonfor the full entries array).
To regrade an assignment after fixing a buggy test or updating an autograder (re-runs the existing submissions on the same commit, so submission times / late flags don't change): trigger the Regrade action in the web app, or run the workflow directly with gh workflow run regrade.yaml -f classroom=<short-name> -f assignment=<slug> (add -f owner=<login> for a single student). Grading runs asynchronously inside each student repo, so the refreshed scores appear only after the next collect-scores.yaml run (nightly cron, or "Collect now").