An automated grading system for GitHub Classroom assignments that clones student repositories, runs test scripts, collects performance metrics, and generates a leaderboard with results.
- Overview
- Features
- Installation
- Configuration
- GitHub Personal Access Token
- Running the Grader
- Setting Up as a Recurrent Task
- Test Script Format
- Output Format
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
- 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
- Clone this repository
- Install dependencies using
uvorpip:uv sync # or pip install -r requirements.txt
Create your configuration file by copying the example:
cp grader-config-example.yaml grader-config.yamlEdit 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 insteadField 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).
Add assignments to the assignments list. Each assignment can have multiple tasks that run different test scripts with different configurations:
assignments:
- name: "vector-sum"
invite_link: "https://classroom.github.com/a/example1"
preserve_repo_files: false
tasks:
- name: "test-cpu"
skip: false
blocking: true
test_script_path: "./test_scripts/test_vectorsum_cpu.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
- name: "test-gpu"
skip: false
blocking: true
test_script_path: "./test_scripts/test_vectorsum_gpu.sh"
slurm_backend:
config:
slurm_partition: "gpu"
timeout_min: 60
mem_gb: 8
nodes: 1
tasks_per_node: 1
cpus_per_task: 4
gpus_per_node: 1Assignment-Level 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.
-
preserve_repo_files(default:false): Iftrue, cloned repositories will not be deleted after grading. Useful for debugging. -
tasks(required): A list of tasks to run for this assignment. Each task represents a different test or evaluation.
Task-Level Field Explanations:
-
name(required): A unique identifier for the task within the assignment. Used to identify the task in logs and output. -
skip(default:false): Iftrue, this task will be skipped during grading. -
blocking(default:false): Iftrue, the grader will wait for this task's execution to complete before proceeding to the next task. Useful for sequential processing or when tasks have dependencies. -
test_script_path(required): Absolute or relative path to the test script that will be executed in each student's repository for this task.
When writing your test script don't forget to launch the student's script with either
srun,mpirunor a similar command that will also schedule the process on the cluster!
The slurm_backend.config section within each task accepts parameters that are passed directly to submitit, which manages SLURM job submissions. Each task can have different SLURM resource requirements.
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 nodeCommon 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.
The grader requires a GitHub Personal Access Token (PAT) with appropriate permissions to access GitHub Classroom data and clone student repositories.
- 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)
You have two options:
Option 1: Environment Variable (Recommended)
export GH_PAT="github_pat_xxxxx"
uv run main.pyOr inline:
GH_PAT="github_pat_xxxxx" uv run main.pyOption 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_patOnce configured, run the grader:
uv run main.py
# or
python main.pyThe 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
To run the grader automatically at regular intervals without administrator privileges, you can use cron (Linux/macOS) or systemd user timers.
-
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>&1
Run every 6 hours:
0 */6 * * * cd /path/to/grader && /usr/bin/uv run main.py >> /path/to/grader/cron.log 2>&1
Run 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
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
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]}'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)
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
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.