Compare commits
2
Commits
1093066598
...
8b550cfe10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b550cfe10 | ||
|
|
34841fa222 |
@@ -80,17 +80,55 @@
|
|||||||
(builtins.readFile ./pigeon.sh);
|
(builtins.readFile ./pigeon.sh);
|
||||||
};
|
};
|
||||||
|
|
||||||
hooks = pkgs.writeText "pigeon-hooks.json"
|
hooks = pkgs.writeText "pigeon-hooks.json" (builtins.toJSON [
|
||||||
(builtins.replaceStrings [ "@pigeon@" ] [ "${pigeon}/bin/pigeon" ]
|
{
|
||||||
(builtins.readFile ./hooks.json));
|
id = "release";
|
||||||
|
execute-command = "${pkgs.nq}/bin/nq";
|
||||||
|
include-command-output-in-response = false;
|
||||||
|
pass-arguments-to-command = [
|
||||||
|
{ source = "string"; name = "${pigeon}/bin/pigeon"; }
|
||||||
|
{ source = "payload"; name = "project.id"; }
|
||||||
|
{ source = "payload"; name = "project.namespace"; }
|
||||||
|
{ source = "payload"; name = "project.name"; }
|
||||||
|
{ source = "payload"; name = "commit.id"; }
|
||||||
|
{ source = "payload"; name = "tag"; }
|
||||||
|
{ source = "payload"; name = "created_at"; }
|
||||||
|
];
|
||||||
|
trigger-rule.and = [
|
||||||
|
{
|
||||||
|
match = {
|
||||||
|
type = "value";
|
||||||
|
parameter = { source = "header"; name = "X-Gitlab-Event"; };
|
||||||
|
value = "Release Hook";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
match = {
|
||||||
|
type = "value";
|
||||||
|
parameter = { source = "payload"; name = "action"; };
|
||||||
|
value = "create";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
match = {
|
||||||
|
type = "value";
|
||||||
|
parameter = { source = "header"; name = "X-Gitlab-Token"; };
|
||||||
|
value = "{{ getenv `PIGEON_GITLAB_SECRET` | js }}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
pigeon-webhook = pkgs.writeShellApplication {
|
pigeon-webhook = pkgs.writeShellApplication {
|
||||||
name = "pigeon-webhook";
|
name = "pigeon-webhook";
|
||||||
runtimeInputs = with pkgs; [ webhook ];
|
runtimeInputs = with pkgs; [ coreutils nq sqlite webhook ];
|
||||||
text = ''
|
text = ''
|
||||||
: "''${PIGEON_GITLAB_SECRET:?}"
|
: "''${PIGEON_GITLAB_SECRET:?}"
|
||||||
: "''${PIGEON_GITLAB_TOKEN:?}"
|
: "''${PIGEON_GITLAB_TOKEN:?}"
|
||||||
: "''${PIGEON_PROJECTS:?}"
|
: "''${PIGEON_PROJECTS:?}"
|
||||||
|
|
||||||
|
export NQDIR=./queue
|
||||||
exec webhook -hooks ${hooks} -template -port 3000 -verbose -debug
|
exec webhook -hooks ${hooks} -template -port 3000 -verbose -debug
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "release",
|
|
||||||
"execute-command": "@pigeon@",
|
|
||||||
"include-command-output-in-response": false,
|
|
||||||
"pass-arguments-to-command": [
|
|
||||||
{ "source": "payload", "name": "project.id" },
|
|
||||||
{ "source": "payload", "name": "project.namespace" },
|
|
||||||
{ "source": "payload", "name": "project.name" },
|
|
||||||
{ "source": "payload", "name": "commit.id" },
|
|
||||||
{ "source": "payload", "name": "tag" }
|
|
||||||
],
|
|
||||||
"trigger-rule": {
|
|
||||||
"and": [
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"type": "value",
|
|
||||||
"parameter": { "source": "header", "name": "X-Gitlab-Event" },
|
|
||||||
"value": "Release Hook"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"type": "value",
|
|
||||||
"parameter": { "source": "payload", "name": "action" },
|
|
||||||
"value": "create"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match": {
|
|
||||||
"type": "value",
|
|
||||||
"parameter": { "source": "header", "name": "X-Gitlab-Token" },
|
|
||||||
"value": "{{ getenv `PIGEON_GITLAB_SECRET` | js }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -3,9 +3,56 @@ NAMESPACE="$2"
|
|||||||
PROJECT="$3"
|
PROJECT="$3"
|
||||||
COMMIT="$4"
|
COMMIT="$4"
|
||||||
TAG="$5"
|
TAG="$5"
|
||||||
|
CREATED_AT="$6"
|
||||||
|
|
||||||
: "${PIGEON_GITLAB_TOKEN:?}"
|
: "${PIGEON_GITLAB_TOKEN:?}"
|
||||||
: "${PIGEON_PROJECTS:?}"
|
: "${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)"
|
DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)"
|
||||||
trap 'rm -rf "$DIRECTORY"' EXIT
|
trap 'rm -rf "$DIRECTORY"' EXIT
|
||||||
@@ -42,18 +89,20 @@ EOF
|
|||||||
firecracker --no-api --level Error --config-file vm.json
|
firecracker --no-api --level Error --config-file vm.json
|
||||||
|
|
||||||
RESULT=""
|
RESULT=""
|
||||||
|
SCORE=0
|
||||||
TESTS="$PIGEON_PROJECTS/$PROJECT/tests"
|
TESTS="$PIGEON_PROJECTS/$PROJECT/tests"
|
||||||
for TEST in "$TESTS"/*.sh ; do
|
for TEST in "$TESTS"/*.sh ; do
|
||||||
debugfs -R "cat /$(basename "$TEST" .sh)" results.img >scratch 2>/dev/null
|
debugfs -R "cat /$(basename "$TEST" .sh)" results.img >scratch 2>/dev/null
|
||||||
if cmp scratch "$TESTS/$(basename "$TEST" .sh).expected" ; then
|
if cmp -s scratch "$TESTS/$(basename "$TEST" .sh).expected" ; then
|
||||||
RESULT="${RESULT}PASS\n"
|
RESULT="${RESULT}PASS\n"
|
||||||
|
SCORE=$((SCORE + 1))
|
||||||
else
|
else
|
||||||
RESULT="${RESULT}FAIL: $(cat "$TESTS/$(basename "$TEST" .sh).hint")\n"
|
RESULT="${RESULT}FAIL: $(cat "$TESTS/$(basename "$TEST" .sh).hint")\n"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
curl -s --show-error --out-null --request PUT \
|
# Record before reporting, so a GitLab outage doesn't lose the result.
|
||||||
--header "PRIVATE-TOKEN: $PIGEON_GITLAB_TOKEN" \
|
db "UPDATE results SET score = $SCORE WHERE id = $ROW;" ||
|
||||||
--header 'Content-Type: application/json' \
|
echo "pigeon: failed to record score for row $ROW in $PIGEON_DB" >&2
|
||||||
--data "{\"description\": \"$RESULT\"}" \
|
|
||||||
--url "https://gitlab.com/api/v4/projects/$ID/releases/$TAG"
|
report "$RESULT"
|
||||||
|
|||||||
Reference in New Issue
Block a user