ID="$1" NAMESPACE="$2" PROJECT="$3" COMMIT="$4" TAG="$5" CREATED_AT="$6" : "${PIGEON_GITLAB_TOKEN:?}" : "${PIGEON_PROJECTS:?}" : "${PIGEON_DB:?}" LIMIT="${PIGEON_RATE_LIMIT:-3}" # graded attempts per student per project per hour PIGEON_DB="$(realpath -m "$PIGEON_DB")" db() { sqlite3 -bail -cmd '.timeout 10000' "$PIGEON_DB" "$@"; } # quote a value as a SQL string literal q() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"; } report() { curl -s --show-error --out-null --request PUT \ --header "PRIVATE-TOKEN: $PIGEON_GITLAB_TOKEN" \ --header 'Content-Type: application/json' \ --data "{\"description\": \"$1\"}" \ --url "https://gitlab.com/api/v4/projects/$ID/releases/$TAG" } mkdir -p "$(dirname "$PIGEON_DB")" db << 'EOF' CREATE TABLE IF NOT EXISTS results ( id INTEGER PRIMARY KEY, student TEXT NOT NULL, project TEXT NOT NULL, commit_sha TEXT NOT NULL, time TEXT NOT NULL DEFAULT (datetime('now')), score INTEGER CHECK (score >= 0) ); EOF # GitLab's "2026-10-16 02:27:46 UTC" -> SQLite's "2026-10-16 02:27:46". # If it's missing or unparseable, fall back to the current time. CREATED="$(date -u -d "$CREATED_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" # Rate limit and record the submission in one atomic statement. # No row comes back if the student is over the limit. ROW="$(db "INSERT INTO results (student, project, commit_sha, time) SELECT $(q "$NAMESPACE"), $(q "$PROJECT"), $(q "$COMMIT"), COALESCE(NULLIF($(q "$CREATED"), ''), datetime('now')) WHERE (SELECT COUNT(*) FROM results WHERE student = $(q "$NAMESPACE") AND project = $(q "$PROJECT") AND time > datetime('now', '-1 hour')) < $LIMIT RETURNING id;")" if [ -z "$ROW" ]; then report "Rate limit reached: at most $LIMIT submissions per hour. Try again later." exit 0 fi DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)" trap 'rm -rf "$DIRECTORY"' EXIT cd "$DIRECTORY" git -c advice.detachedHead=false clone -q --rev "$COMMIT" --depth 1 \ "https://oauth2:$PIGEON_GITLAB_TOKEN@gitlab.com/$NAMESPACE/$PROJECT.git" student/ rm -rf student/.git mkdir -p project/tests project/build cp -a "$PIGEON_PROJECTS/$PROJECT"/tests/*.sh project/tests/ cp -a "$PIGEON_PROJECTS/$PROJECT"/build/. project/build/ mkfs.ext4 -q -F -d student student.img 16M mkfs.ext4 -q -F -d project project.img 16M mkfs.ext4 -q -F results.img 16M cat >vm.json <<'EOF' { "boot-source": { "kernel_image_path": "@kernel@", "boot_args": "reboot=k panic=1 init=/init quiet nomodule i8042.noaux i8042.nomux i8042.dumbkbd swiotlb=noforce console=ttyS0" }, "drives": [ { "drive_id": "rootfs", "path_on_host": "@rootfs@", "is_root_device": true, "is_read_only": true }, { "drive_id": "student", "path_on_host": "student.img", "is_root_device": false, "is_read_only": true }, { "drive_id": "project", "path_on_host": "project.img", "is_root_device": false, "is_read_only": true }, { "drive_id": "results", "path_on_host": "results.img", "is_root_device": false, "is_read_only": false } ], "machine-config": { "vcpu_count": 1, "mem_size_mib": 512 } } EOF firecracker --no-api --level Error --config-file vm.json RESULT="" SCORE=0 TESTS="$PIGEON_PROJECTS/$PROJECT/tests" for TEST in "$TESTS"/*.sh ; do debugfs -R "cat /$(basename "$TEST" .sh)" results.img >scratch 2>/dev/null if cmp -s scratch "$TESTS/$(basename "$TEST" .sh).expected" ; then RESULT="${RESULT}PASS\n" SCORE=$((SCORE + 1)) else RESULT="${RESULT}FAIL: $(cat "$TESTS/$(basename "$TEST" .sh).hint")\n" fi done # Record before reporting, so a GitLab outage doesn't lose the result. db "UPDATE results SET score = $SCORE WHERE id = $ROW;" || echo "pigeon: failed to record score for row $ROW in $PIGEON_DB" >&2 report "$RESULT"