Small standalone scripts for common GitHub course repository administration tasks.
This repository is shared in response to uncertainty around GitHub Classroom workflows and availability. See the related GitHub Community discussion: https://github.com/orgs/community/discussions/196615
The goal is not to replace GitHub Classroom or become a large framework. The goal is to provide a small, understandable toolkit that instructors can copy, configure, dry-run, inspect, and run safely.
- github-course-repo-assistant
Use this toolkit when you want to:
- Create one private repository per student from a template repository.
- Create one private repository per team from a template repository.
- Invite students to those repositories.
- Check current repository permissions without changing anything.
- Make student access read-only after a deadline.
- Make student access writable again when needed.
This toolkit intentionally uses a few standalone scripts instead of one large application. Each script has its own matching YAML config file so instructors can see exactly which settings affect the task they are running.
The safe workflow is always the same:
- Edit the matching
.ymlfile. - Keep
dryRun: true. - Run the matching
.jsscript. - Check the terminal output and CSV report.
- Change
dryRuntofalseonly when the dry run looks right. - Run again and type
yesto confirm real GitHub changes.
create-repos.js Creates repos from a template and invites students
create-repos.yml Settings for create-repos.js
update-permissions.js Reports or changes student permissions
update-permissions.yml Settings for update-permissions.js
students-course101.example.csv Example course roster
.env.example Example GitHub token file
package.json Node.js dependencies and commands
LICENSE MIT License
Each script has its own YAML file with the same name. This keeps configuration local to the task an instructor is running.
If you manage multiple courses, assignments, or semesters, it'll be easier to create copies of the YAML and CSV files with descriptive names, such as create-repos-course101-assignment1.yml and students-course101-fall2026.csv, so each course or assignment can keep its own roster, repository prefix, and dry-run settings. This way you don't need to edit YAML files back and forth or worry about accidentally running a script with the wrong settings. See the Managing Multiple Courses section below for more on that.
You need Node.js 18 or newer and a GitHub token that can create repositories and manage collaborators in your GitHub organization.
From this folder, install dependencies:
npm installCreate your local token file:
cp .env.example .envEdit .env:
GITHUB_TOKEN=your_token_here
Do not upload .env to GitHub. It is ignored by .gitignore.
The scripts use GITHUB_TOKEN from your local .env file. The token should belong to an instructor or service account that is allowed to administer the course repositories.
GitHub's token documentation is here: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
The token needs enough access to:
- Read organization repositories and repository teams.
- Create private repositories in the organization from a template repository.
- Invite collaborators to repositories.
- Update repository collaborator, invitation, and team permissions.
For a classic personal access token, instructors commonly need scopes such as repo and admin:org, depending on organization settings.
Treat the token like a password. Keep it only in .env, and rotate it if it is exposed.
Start by copying the example roster:
cp students-course101.example.csv students-course101.csvThe example columns are:
StudentName,StudentID,GitHubUsername,TeamName
Example Student One,100000001,example-student-one,1
Example Student Two,100000002,example-student-two,1
Example Student Three,100000003,example-student-three,2Both YAML files can point to the same roster file:
studentCsv: "students-course101.csv"create-repos.js uses the roster to decide which repositories to create and which students to invite.
update-permissions.js reports current repository access or changes student access to read-only or writable for matching repositories. By default, it uses the roster to avoid changing direct collaborators or pending invitations for people who are not listed as students, which helps protect instructor and TA accounts.
For one repository per student, use these roster columns:
StudentNameStudentIDGitHubUsername
Your roster may still include TeamName if the same CSV is also used for team assignments in the same course. In individual mode, create-repos.js ignores TeamName and does not allow {TeamName} in repoNamePattern.
For one repository per team, use these roster columns:
StudentNameStudentIDGitHubUsernameTeamName
Students with the same TeamName value are invited to the same repository.
Edit create-repos.yml first:
org: "YOUR-GITHUB-ORGANIZATION"
templateRepo: "assignment-template" # The template repository must already exist in the organization.
studentCsv: "students-course101.csv"
dryRun: true # Set to false only when you are ready to create repositories and invite students.The template repository must already exist and be marked as a template repository on GitHub (go to the repository Settings and check "Template repository"). The script creates new repositories from the template, so any starter code, README content, or other files in the template are copied into each student repository when it is created. If there are multiple commits in the template, the new repositories will squash that history into one commit.
This toolkit creates repositories from a template instead of forking starter code. That means later starter-code changes cannot be synced into already-created student repositories with GitHub's fork sync tools. This is an intentional tradeoff: fork-based classroom workflows can create serious academic misconduct risks when student repositories remain connected through fork relationships. See the related Global Campus Teachers discussion: https://github.com/community/Global-Campus-Teachers/discussions/368
Run a dry run. Always pass the YAML config file explicitly:
node create-repos.js create-repos.ymlWhen the output and report look right, change dryRun to false and run again:
node create-repos.js create-repos.ymlType yes only when you are ready to create repositories or invite students.
After sending invitations, tell students to accept them as soon as possible. GitHub repository invitations expire after seven days. If some students miss the window, rerun create-repos.js with the same config. The script checks accepted collaborators and active pending invitations first, so it will not create duplicate access for students who already accepted or already have a matching pending invitation. If an invitation expired or is otherwise missing, the script sends the invite again.
At the end of the run, create-repos.js checks for active invitations that still have not been accepted. It prints a short console summary and includes unaccepted-invitation rows in the CSV report so instructors have a file they can use for follow-up.
Use this when each student gets a separate repository:
mode: "individual"
repoNamePattern: "assignment-1-{GitHubUsername}"For GitHub username example-student-one, this creates:
assignment-1-example-student-one
Individual repository creation uses StudentName, StudentID, and GitHubUsername. It ignores TeamName, even when the roster includes that column for other assignments.
Available individual repository-name placeholders are {GitHubUsername}, {StudentName}, and {StudentID}.
For individual repositories, prefer a pattern with one unique identifier, such as {GitHubUsername} or {StudentID}. Student names are not guaranteed to be unique, so avoid using {StudentName} as the only student-specific part of the repository name. It is also usually easier to manage repository names later if you use one identifier rather than combining several identifiers with multiple separators.
Placeholder values are cleaned before they become part of a repository name: leading and trailing spaces are removed, spaces in the middle become hyphens, and characters that are not safe in GitHub repository names are replaced. For example, Mary Ann Smith becomes Mary-Ann-Smith.
The default pattern uses the student's GitHub username:
repoNamePattern: "assignment-1-{GitHubUsername}"If your course tracks students primarily by institutional ID, this is also a good simple pattern:
repoNamePattern: "assignment-1-{StudentID}"Use this when students work in teams:
mode: "team"
teamRepoNamePattern: "assignment-1-team-{TeamName}"With the default settings, numeric-only TeamName values are padded to two digits. Team 1 becomes 01, so the repository is:
assignment-1-team-01
The padding amount is configurable in create-repos.yml:
padTeamNumbers: true
teamNumberPadding: 2Set teamNumberPadding: 0 if you do not want numeric team names padded. With that setting, team 1 stays 1.
Team repositories use the standard roster columns, including TeamName to group students into repositories.
Available repository-name placeholders are {GitHubUsername}, {StudentName}, {StudentID}, and {TeamName}.
The same repository-name cleanup applies to non-numeric team names. For example, Project Team 1 becomes Project-Team-1. It is not padded because the full value is not just a number.
Use update-permissions.js after a deadline, or whenever you need to check or change student access for matching repositories.
Edit update-permissions.yml:
org: "YOUR-GITHUB-ORGANIZATION"
repoPrefix: "assignment-1-"
studentCsv: "students-course101.csv"
permission: "print-only"
dryRun: trueThe script finds non-template repositories whose names start with repoPrefix.
Use a specific prefix, such as assignment-1-, so the script only reports or changes the repositories for the intended assignment.
By default, direct collaborators and pending invitations are changed only if their GitHub username appears in studentCsv:
onlyUsersFromCsv: trueIf instructor or TA accounts may have non-admin access, add them to skipUsers:
skipUsers:
- "ta-github-username"If an organization team should never be changed, add either its name or slug to skipTeams:
skipTeams:
- "teaching-staff"Admin users and admin teams are always skipped.
Use this first to see what the script finds:
permission: "print-only"
dryRun: trueRun:
node update-permissions.js update-permissions.ymlNo permissions are changed in print-only mode.
Use this after a deadline:
permission: "read-only"
dryRun: trueRun the dry run first, check the report, then set:
dryRun: falseRun again and type yes to confirm.
Use this when students need push access again:
permission: "write"
dryRun: trueRun the dry run first, check the report, then set dryRun: false and run again.
For multiple courses, assignments, or semesters, copy the YAML and CSV files and keep the names descriptive:
students-course101-fall2026.csv
create-repos-course101-assignment1.yml
update-permissions-course101-assignment1.yml
Always pass the config file name when running a script:
node create-repos.js create-repos-course101-assignment1.yml
node update-permissions.js update-permissions-course101-assignment1.ymlThis lets each course or assignment keep its own roster, repository prefix, and dry-run settings.
Both scripts write CSV reports into the reports/ folder.
Reports show what was found, what would change during a dry run, what changed during a real run, and any GitHub API errors.
The reports/ folder is ignored by Git by default.
GitHub limits how many API requests a token can make in a period of time. For larger courses, these scripts may need to make many requests while checking repositories, inviting students, listing permissions, or updating access.
The scripts watch GitHub's primary rate-limit response headers. If GitHub reports that the limit is exhausted, the script prints a message and sleeps until GitHub's reported reset time, with a small buffer, before continuing. This can look like the script is paused for several minutes, but it is waiting so the run can continue safely instead of failing partway through.
GitHub's REST API rate-limit documentation is here: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
Create a .env file and add:
GITHUB_TOKEN=your_token_here
Make sure you are running the command from this folder, and that the .yml file exists.
Both scripts require the config file path. For example:
node create-repos.js create-repos.yml
node update-permissions.js update-permissions.ymlCheck the studentCsv value in the YAML file you are using.
Your roster column names do not match the csvColumns section in the YAML file. Either rename the CSV columns or update the config.
For create-repos.js, the script checks the organization first, then the template repository, and prints which GitHub resource it was trying to access.
Common causes:
- The organization name is wrong.
- The token account cannot access the organization.
- The template repository name is wrong.
- The template repository exists but is not visible to the token account.
- The template repository is not marked as a template repository in GitHub settings.
- The token does not have access to the organization or repository.
- The token belongs to an account that cannot administer the target repositories.
The token is missing, expired, copied incorrectly, or does not have the needed access.