diff --git a/flake.nix b/flake.nix index 960135f..7b1747c 100644 --- a/flake.nix +++ b/flake.nix @@ -119,16 +119,17 @@ ]; } ]); - pigeon-webhook = pkgs.writeShellApplication { name = "pigeon-webhook"; - runtimeInputs = with pkgs; [ coreutils nq sqlite webhook ]; + runtimeInputs = with pkgs; [ coreutils jq nq sqlite webhook ]; text = '' : "''${PIGEON_GITLAB_SECRET:?}" : "''${PIGEON_GITLAB_TOKEN:?}" : "''${PIGEON_PROJECTS:?}" + : "''${PIGEON_STATE}" - export NQDIR=./queue + mkdir -p "$PIGEON_STATE" + export NQDIR="$PIGEON_STATE/nqdir" exec webhook -hooks ${hooks} -template -port 3000 -verbose -debug ''; }; diff --git a/pigeon.sh b/pigeon.sh index d58051e..0e2cf6d 100755 --- a/pigeon.sh +++ b/pigeon.sh @@ -1,70 +1,71 @@ -ID="$1" -NAMESPACE="$2" -PROJECT="$3" -COMMIT="$4" -TAG="$5" -CREATED_AT="$6" +PROJECT_ID="$1" +PROJECT_NAMESPACE="$2" +PROJECT_NAME="$3" +RELEASE_COMMIT="$4" +RELEASE_TAG="$5" +RELEASE_TIME="$6" : "${PIGEON_GITLAB_TOKEN:?}" : "${PIGEON_PROJECTS:?}" -: "${PIGEON_DB:?}" -LIMIT="${PIGEON_RATE_LIMIT:-3}" # graded attempts per student per project per hour +: "${PIGEON_STATE:?}" -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")"; } +RATE_LIMIT=3 + +PIGEON_DATABASE="$(realpath -m "$PIGEON_STATE/db")" +RELEASE_TIME="$(date -d "$RELEASE_TIME" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" 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" + --data "$(jq -n --arg d "$1" '{"description": $d}')" \ + --url "https://gitlab.com/api/v4/projects/$PROJECT_ID/releases/$RELEASE_TAG" +} +db() { + sqlite3 -bail -cmd '.timeout 10000' "$PIGEON_DATABASE" "$@" +} +q() { + printf "'%s'" "$(printf %s "$1" | sed "s/'/''/g")" } -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, + hash 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)" +ROW="$(db < datetime('now', '-1 day')) < $RATE_LIMIT +RETURNING id; +EOF +)" -# 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 +if [ -z "$ROW" ] ; then + report "Rate limit exceeded!" + exit 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/ +git -c advice.detachedHead=false clone -q --rev "$RELEASE_COMMIT" --depth 1 \ + "https://oauth2:$PIGEON_GITLAB_TOKEN@gitlab.com/$PROJECT_NAMESPACE/$PROJECT_NAME.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/ +cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/tests/*.sh project/tests/ +cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. project/build/ mkfs.ext4 -q -F -d student student.img 16M mkfs.ext4 -q -F -d project project.img 16M @@ -90,7 +91,7 @@ firecracker --no-api --level Error --config-file vm.json RESULT="" SCORE=0 -TESTS="$PIGEON_PROJECTS/$PROJECT/tests" +TESTS="$PIGEON_PROJECTS/$PROJECT_NAME/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