An automatic, fully configurable grader created as part of my TA scolarship for the multicore programming course
GitHub Classroom Autograder
An automated grading system for GitHub Classroom assignments that clones student repositories, runs test scripts, collects performance metrics, and generates a leaderboard with results.
Table of Contents
- Overview
- Features
- Installation
- Configuration
- GitHub Personal Access Token
- Running the Grader
- Setting Up as a Recurrent Task
- Test Script Format
- Output Format
Overview
This grading system automates the process of:
- Fetching student submissions from GitHub Classroom
- Cloning student repositories
- Running test scripts with configurable SLURM resources
- Collecting results including pass/fail status and performance metrics
- Generating a JSON leaderboard with all results
Features
- Automated GitHub Classroom Integration: Fetches assignments and submissions via GitHub API
- SLURM Support: Execute test scripts with configurable SLURM resources (CPU, memory, GPUs, etc.)
- Performance Metrics: Collects runtime statistics from test scripts
- Flexible Assignment Identification: Use invite links, slugs, or assignment IDs
- Blocking/Non-blocking Execution: Control whether to wait for assignment completion
- Repository Cleanup: Option to preserve or delete cloned repositories
- Leaderboard Generation: JSON output with all results and metrics
Installation
- Clone this repository
- Install dependencies using
uvorpip:uv sync # or pip install -r requirements.txt
Configuration
Step 1: Copy the Example Configuration
Create your configuration file by copying the example:
cp grader-config-example.yaml grader-config.yaml
Step 2: Configure the Grader Section
Edit the grader section in grader-config.yaml:
grader:
working_dir: "/tmp/grader"
grades_file: "leaderboard.json"
github_pat: "github_pat_xxxxx" # Optional, can use ENV variable instead
Field Explanations:
working_dir: Directory where student repositories will be cloned. This directory must exist before running the grader.grades_file: Path to the output JSON file containing all grading results and the leaderboard.github_pat: (Optional) Your GitHub Personal Access Token. Can be omitted if provided via environment variable (see GitHub Personal Access Token section).
Step 3: Configure Assignments
Add assignments to the assignments list:
assignments:
- name: "vector-sum"
invite_link: "https://classroom.github.com/a/example1"
skip: false
preserve_repo_files: false
blocking: true
test_script_path: "./test_scripts/test_vectorsum.sh"
slurm_backend:
config:
slurm_partition: "short"
timeout_min: 60
mem_gb: 4
nodes: 1
tasks_per_node: 1
cpus_per_task: 2
gpus_per_node: 0
Field Explanations:
-
name(required): A unique identifier for the assignment. Used as the key in the output JSON. -
Assignment Identification (at least one required):
invite_link: The GitHub Classroom assignment invite URL (e.g.,https://classroom.github.com/a/xxxxx)slug: The assignment slug from GitHub Classroom (alternative to invite_link)id: The numeric assignment ID (alternative to invite_link or slug)
Note: You only need to provide one of these three identifiers.
-
skip(default:false): Iftrue, this assignment will be skipped during grading. -
preserve_repo_files(default:false): Iftrue, cloned repositories will not be deleted after grading. Useful for debugging. -
blocking(default:false): Iftrue, the grader will wait for this assignment's grading to complete before proceeding to the next assignment. Useful for sequential processing or when assignments have dependencies. -
test_script_path(required): Absolute or relative path to the test script that will be executed in each student's repository.
Step 4: Configure SLURM Backend
The slurm_backend.config section accepts parameters that are passed directly to submitit, which manages SLURM job submissions.
slurm_backend:
config:
slurm_partition: "short" # SLURM partition name
timeout_min: 120 # Maximum runtime in minutes
mem_gb: 8 # Memory allocation in GB
nodes: 1 # Number of nodes
tasks_per_node: 1 # Tasks per node
cpus_per_task: 8 # CPUs per task
gpus_per_node: 0 # GPUs per node
Common Parameters (from submitit):
slurm_partition: SLURM partition/queue nametimeout_min: Job timeout in minutesmem_gb: Memory allocation per node in gigabytesnodes: Number of compute nodestasks_per_node: Number of MPI tasks per nodecpus_per_task: Number of CPU cores per taskgpus_per_node: Number of GPUs per nodeslurm_account: SLURM account to charge (if required)slurm_qos: Quality of Service specification
For a complete list of available parameters, refer to the submitit documentation.
GitHub Personal Access Token
The grader requires a GitHub Personal Access Token (PAT) with appropriate permissions to access GitHub Classroom data and clone student repositories.
Creating a PAT
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Generate a new token with the following scopes:
repo(Full control of private repositories)read:org(Read organization data)admin:org(if managing classroom assignments)
Providing the PAT
You have two options:
Option 1: Environment Variable (Recommended)
export GH_PAT="github_pat_xxxxx"
uv run main.py
Or inline:
GH_PAT="github_pat_xxxxx" uv run main.py
Option 2: Configuration File
Add it directly to grader-config.yaml:
grader:
github_pat: "github_pat_xxxxx"
As shown in main.py, the environment variable takes precedence over the config file value:
token = os.getenv("GH_PAT") or config.config.grader.github_pat
Running the Grader
Once configured, run the grader:
uv run main.py
# or
python main.py
The grader will:
- Read the configuration file
- For each assignment (unless skipped):
- Fetch student submissions from GitHub Classroom
- Clone each student's repository
- Copy the test script to the repository
- Execute the test script
- Parse the output for test results and performance metrics
- Clean up (unless
preserve_repo_filesis true)
- Save all results to the
grades_file
Setting Up as a Recurrent Task
To run the grader automatically at regular intervals without administrator privileges, you can use cron (Linux/macOS) or systemd user timers.
Option 1: Using Cron
-
Open your crontab for editing:
crontab -e -
Add a cron job to run the grader. Examples:
Run every day at 2 AM:
0 2 * * * cd /path/to/grader && /usr/bin/uv run main.py >> /path/to/grader/cron.log 2>&1Run every 6 hours:
0 */6 * * * cd /path/to/grader && /usr/bin/uv run main.py >> /path/to/grader/cron.log 2>&1Run every Monday at 9 AM:
0 9 * * 1 cd /path/to/grader && /usr/bin/uv run main.py >> /path/to/grader/cron.log 2>&1 -
Make sure to:
- Use absolute paths for both the grader directory and the
uvexecutable - Export the
GH_PATenvironment variable in the cron job if not using the config file:0 2 * * * cd /path/to/grader && GH_PAT="github_pat_xxxxx" /usr/bin/uv run main.py >> /path/to/grader/cron.log 2>&1
- Use absolute paths for both the grader directory and the
Option 2: Using Systemd User Timer
Systemd user timers don't require root privileges and offer more control.
-
Create a service file at
~/.config/systemd/user/grader.service:[Unit] Description=GitHub Classroom Autograder [Service] Type=oneshot WorkingDirectory=/path/to/grader Environment="GH_PAT=github_pat_xxxxx" ExecStart=/usr/bin/uv run main.py [Install] WantedBy=default.target -
Create a timer file at
~/.config/systemd/user/grader.timer:[Unit] Description=Run GitHub Classroom Autograder daily [Timer] OnCalendar=daily Persistent=true [Install] WantedBy=timers.target -
Enable and start the timer:
systemctl --user daemon-reload systemctl --user enable grader.timer systemctl --user start grader.timer -
Check timer status:
systemctl --user list-timers systemctl --user status grader.timer
Timer Schedule Examples:
OnCalendar=daily- Run once per day at midnightOnCalendar=*-*-* 02:00:00- Run daily at 2 AMOnCalendar=Mon *-*-* 09:00:00- Run every Monday at 9 AMOnCalendar=*-*-* 00/6:00:00- Run every 6 hours
Test Script Format
Your test scripts must output results in the following JSON format on the last line of stdout:
{"passed": 12, "total": 12, "times": [442.44, 664.42, 886.58]}
Required Fields:
passed: Number of tests passed (integer)total: Total number of tests (integer)times: Array of runtime measurements in milliseconds (array of floats)
Example Test Script Output:
#!/bin/bash
# ... test execution ...
echo "Test 1: PASSED"
echo "Test 2: PASSED"
echo "Test 3: FAILED"
# Last line must be JSON
echo '{"passed": 2, "total": 3, "times": [123.45, 234.56, 345.67]}'
Output Format
The grader produces a JSON file (specified in grades_file) with the following structure:
{
"assignment-name": [
{
"name": "Student Name",
"commit_hash": "abc1234",
"status": "graded",
"error": "",
"stdout": "Test output...",
"avg_runtime": 123.456,
"data": {
"passed": 10,
"total": 10,
"times": [100.0, 120.5, 150.3]
}
}
]
}
Field Descriptions:
name: Student name(s) associated with the submissioncommit_hash: Git commit hash (first 7 characters) of the graded submissionstatus: Grading status ("graded"or"error")error: Error message if status is"error", empty otherwisestdout: Complete output from the test scriptavg_runtime: Average of all runtime values from thetimesarray (in milliseconds)data: The parsed JSON output from the test script (see Test Script Format)
Troubleshooting
Issue: "GitHub Personal Access Token (PAT) not provided"
- Make sure you've set the
GH_PATenvironment variable or addedgithub_patto the config file - Verify the token has the correct permissions
Issue: "working_dir does not exist"
- Create the directory specified in
working_dirbefore running:mkdir -p /tmp/grader
Issue: Test script not found
- Verify the
test_script_pathpoints to an existing file - Use absolute paths or ensure relative paths are correct from the grader directory
Issue: Test results not showing in output
- Ensure your test script outputs valid JSON on its last line
- Check that the JSON format matches exactly:
{"passed": X, "total": Y, "times": [...]} - Review test script stdout in the leaderboard JSON for debugging
Issue: Cron job not running
- Check cron logs:
grep CRON /var/log/syslog(Linux) orlog show --predicate 'process == "cron"' --last 1h(macOS) - Verify absolute paths in the crontab entry
- Ensure the cron output log file is writable
License
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
Copyright (C) 2026
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.