I started replacing GitHub Classroom before I knew it was dying.
What started it was a semester of COMP3130 in which my students could not get their repositories. They would follow the assignment link, accept, and get a 500. Not all of them, and not every time, which is worse than a clean failure: there is nothing to announce, no workaround to give the cohort, just a stream of individual students who cannot start work and a tutor telling them to try again later.
I raised it with GitHub. The support was very poor. And I could do nothing myself, because I owned none of the moving parts: not the service doing the provisioning, not the retry, not even a way to see which students were stuck. The product was already in maintenance mode, though nobody had told us that yet. My only real option was to wait.
So I started building the thing I could actually fix. The retirement announcement arrived months later and turned a private project into a timely one. I mention the order because it matters for what follows: this is not a list of features assembled to fill a gap in a market.
And once I was building it, the interesting problem turned out not to be the one that made me angry. Provisioning that fails is a bug, and bugs are fixable. Underneath it was something structural, a question none of the replacement announcements mention: which GitHub account belongs to which enrolled student?
GitHub Classroom now shuts down on 28 August 2026. In the thread where GitHub announced it, nine people posted replacements within a fortnight, and every one of them described the same feature set: take a template repository, make a copy per student, put it in an organisation, run the tests. That is the part everyone agrees on, and it is not the hard part.
How Classroom answered it
When a student accepted an assignment, GitHub Classroom showed them a list of every identifier in the class roster and asked them to click their own. Whatever they clicked became the permanent link between a GitHub account and a person in the enrolment.
Look at what that asks. At the exact moment a student is least invested, three clicks into a link a tutor posted, on an unfamiliar site, being asked an administrative question they do not understand the consequences of, the system takes their answer as authoritative and writes it down forever.
Three things went wrong with that in COMP3130.
Students picked the wrong name. Names cluster. Rosters are alphabetical. Nobody reads carefully. A student picks the row above theirs and now their repository is attributed to a classmate, and the classmate's real repository, when it eventually arrives, collides with it.
Students had two GitHub accounts. This is far more common than people who have one GitHub account expect. There is the account from a bootcamp two years ago, with a handle they are now embarrassed by, and the tidy one they made for this unit. They accept the assignment on whichever is logged in, realise it was the wrong one, and discover there is no undo. The acceptance is a permanent record. From there the options are a support ticket to a product in maintenance mode, or a tutor manually untangling it.
Nothing mapped a student number to a repository. Marking needs to go from a row in the LMS to a repository on GitHub. Classroom stored a GitHub login and an email address it had matched at accept time. When those disagreed with the enrolment, and they did, resolution was a human reading two spreadsheets.
None of these are exotic. They are the boring, recurring, hours-per-term kind of problem that never makes it into a feature comparison because it is not a feature. It is the absence of one.
The inversion
The fix is not a better roster picker. It is noticing that the student is being asked to assert something they should have to prove instead.
Two facts must be established: who is this person and which GitHub account is theirs. Classroom established the first by asking, and the second by observing which account happened to be logged in. Both should be the other way around.
Who they are is already known. Every student arrives with an institutional identity, an email address the institution issued and controls. If the student signs in with it, and the enrolment data is keyed by it, the identity is established before any GitHub account enters the picture. Nobody picks anything from a list, because there is no list.
Which GitHub account is theirs is a question GitHub itself will answer, for free, correctly, through OAuth. If the student links the account rather than selecting it from a dropdown or typing the handle, the answer comes from GitHub holding a valid session for that account. It cannot be a typo and it cannot be someone else's.
Then the third piece, which mattered more than expected:
Stop treating multiple accounts as an error. Once identity comes from the institutional sign-in, a student is one person who may hold several GitHub accounts. That is just true, so model it. Now provisioning from the wrong account is not a failure state needing a support ticket, it is a mistake fixed by provisioning again from the right account. Both repositories exist, both are attributed to the same person, and the marker can see both and choose. The entire category of "I accepted with the wrong account" support request stops existing, not because it was fixed but because the data model no longer forbids it.
And once a person is a first-class object holding an institutional identity, a student number, and a set of GitHub accounts, "whose repository is this?" is a lookup instead of an investigation.
And the 500s
The bug that started all this got an answer too, though a less interesting one. Provisioning is a queued job rather than something that happens while a student waits on a request. The worker claims the repository in the database before it makes a single call to GitHub, so a failure halfway through leaves a partial record rather than a mystery, and re-running resumes from that record instead of creating a second repository. A student whose provision fails can simply provision again. Staff can repair one from the assignment page, or re-run the whole class in bulk.
None of that makes GitHub's API stop failing. It means a failure is a thing someone can act on, in the ten minutes before a lab, rather than a support ticket to a product that is not listening.
The part worth taking, even if you use something else
The identity inversion is not specific to this implementation. If you are choosing a Classroom replacement, the question worth asking each candidate is not whether it can distribute repositories, because they all can. It is:
How does this tool decide which GitHub account belongs to which student, and what happens when it gets it wrong?
If the answer involves a student choosing their own name from a list, or a teacher typing a GitHub handle into a field, you have bought the same problem with a new interface. Ask what the undo looks like. In GitHub Classroom there wasn't one.