anygrade GitHub

A single Go binary that turns any git repo of course tasks into a grading system.

Students push. It grades. You teach.

Point anygrade at a course repo and it serves a git interface for submissions, a live web UI for students and teachers, and a local mode for offline self-checks. Behavior is driven by YAML metadata, not code changes. Any language works.

How it works

A familiar clone / edit / commit / push loop for students. No new tools to learn.

  1. 1

    Tasks live in a git repo

    The teacher keeps tasks in a plain git repo. Each task is a directory with a task.yaml that declares its checks, score, deadline, and limits.

  2. 2

    Each student gets a personal clone

    Every student gets a personal server-side clone of the course. They clone it, edit their solution files, commit, and push - over SSH or smart HTTP.

  3. 3

    A push hook detects submissions

    The hook diffs the pushed head against the last processed commit, maps changed paths to tasks, and queues one submission per changed task. Feedback starts right in the push output.

  4. 4

    A worker grades in a clean workspace

    A worker assembles authoritative task files + the student's solution_files + hidden tests, runs the checks in Docker or on the host, and streams results live in the web UI.

Tamper-resistant by construction. Editing open tests, task.yaml, or build files in the student repo is ineffective: the authoritative versions are restored before checking, and such edits are logged for the teacher. Pushes are never rejected for policy reasons - deadlines and attempt limits reject the submission, reported in the push output, not the push itself.

Features

Metadata-driven and language-agnostic. A check is an arbitrary command, so any language runs if the environment can run it.

For teachers

  • YAML-configured. Courses and tasks are configured with course.yaml / task.yaml - no plugin code.
  • Any language. A check is an arbitrary command run in a Docker image or on the host.
  • Score matrix. Students by tasks, with click-through to any student's submitted code.
  • Manual overrides. Adjust a score with a comment; export everything as CSV.
  • Hidden tests. Extra tests from a private repo or local path, with an offline cache fallback - students never see them.
  • Deadlines & limits. Soft/hard deadlines with late penalties, attempt limits, cooldowns, and best or latest scoring.
  • Live queue. Watch, cancel, and recheck submissions as they run. Optional leaderboard with anonymized aliases.

For students

  • Just git. Clone, edit, commit, push. Feedback starts in the push output.
  • Live results. Per-task status, submission history, per-check results, penalty breakdown, and streaming logs while checks run.
  • Self-check offline. anygrade check runs the open tests locally before you push.
  • Easy rechecks. A [recheck <task-id>] commit marker or a button on the task page.
  • Bilingual UI. English or Russian, with a per-user switcher.

Quick start

Requirements: the git binary and optionally Docker (colima on macOS). Building from source needs Go 1.26+.

1. Install

Linux and macOS, amd64 and arm64 - every build is on the releases page.

# linux/amd64
curl -fsSL https://github.com/ekalinin/anygrade/releases/download/latest/anygrade_latest_linux_amd64.tar.gz | tar xz
sudo install anygrade /usr/local/bin/

# or build it yourself
go install github.com/ekalinin/anygrade/cmd/anygrade@latest

2. Serve a course

Run in the course repo, and add .anygrade/ to its .gitignore.

./anygrade validate
./anygrade user add --login prof --role teacher
./anygrade serve --http-addr :8080 --ssh-addr :2222

3. Students clone and push

The personal token is the git-over-HTTP password and the web login. SSH auth is by key only.

# username = login, password = the token
git clone http://host:8080/git/<login>/course.git
git remote add upstream http://host:8080/git/course.git
# once an SSH key is added, at activation or in settings
git clone ssh://git@host:2222/<login>/course.git
git remote add upstream ssh://git@host:2222/course.git

A task, in YAML

Raw score = score × (passed weight / total weight). A required check gates the run.

name: "Intro task"
score: 100

solution_files:        # allowlist of student-editable paths
  - main.go

deadline:
  soft: 2026-09-24T23:59:59+03:00
  hard: 2026-10-01T23:59:59+03:00
  penalty: {percent: 10, per: 24h, max_percent: 50}

checks:
  - name: build
    required: true     # failure stops the run, submission scores 0
    run: go build ./...
  - name: basic
    weight: 60
    run: go test -run 'TestBasic' ./...
  - name: advanced
    weight: 40
    run: go test -run 'TestAdvanced' ./...

More in the README and the full specification.

Security & why it's different

Student code is untrusted, and the runner treats it that way.

Sandboxed checks

One ephemeral Docker container per check: memory/cpu/pids limits, no network by default, read-only base image, non-root user, tmpfs workspace, and a hard wall-clock timeout. Serving untrusted code on the local runner over a non-loopback address refuses to start without an explicit flag.

Locked-down auth

Tokens and invite links are stored hashed. SSH is limited to git commands. Failed logins are rate-limited per client and login across both git and the web. Role checks on every route - students see 404, not 403, on teacher pages.

One binary, one directory

SQLite storage and embedded web assets ship in a single Go binary. Everything mutable lives in one data dir - backup is a copy of the directory. The only external dependencies are git and, optionally, Docker.

Self-hosted, no lock-in

No external identity provider, no LMS coupling - CSV export is the integration point. One running instance serves exactly one course, on your own hardware.

No git server to run

Nothing to install or configure - no GitLab, no Gitea. anygrade is the git server: it wraps the system git over SSH and smart HTTP, so pushes just work. Point it at a course repo and go.

Light on resources

Go under the hood: one static binary with SQLite and embedded assets - no JVM, language runtime, or interpreter to feed. The footprint stays small enough for a modest VM.