t5ugu です。
MMA Advent Calender 2025 21日目の記事です。
昨日は sadatch さんの「一般ヲタクによるコミケ巡回の流儀」でした。
コミケには一度は行ってみたいですね。
今日の記事は、予定とは変わって、GitHub Actions 体験記です。
TL;DR
GitHub Actions を使った、アルゴリズムコンテスト向けのジャッジシステムを考えてみます。
成果はこちら。
はじめに
プログラミングコンテストは、ジャッジ環境の基盤の上で成り立っています。
自作を通して、既存のものが優秀なことを実感してみましょう。
自分でホストする方法は Docker とかがあります。
今回は、CI/CD でおなじみの GitHub Actions を使って作りました。
GitHub も、競技プログラミングも初心者ですので、試行錯誤の過程を生暖かい目で見ていただけると幸いです。
コンテストに関わるすべての要素をインターネット上に配置する必要があります。
テクニックによって問題やケースが漏洩すれば、競技性が著しく損なわれるでしょう。
作り方
概観
まずは、いくつかのドキュメントや記事を読むことで、GitHub Actions は工夫次第で何でもできそうだ、という気持ちになります。
競技プログラミングのためのジャッジ環境の基本要件として、
- 問題・解説・実行結果など、人間が読む部分
- テストケースと期待する出力の集まり
- プログラムを受け取り、実行し、結果を得る
の3つのセクションがあると分析しました。
このうち、1 つめの人間が読む部分はフロントエンドに託せばよく、凝るのは非本質的です。
また、2 つめは、できれば制約から自動でテストケースを作る機構もあるとよいですが、最悪 Writer が頑張れば用意できます。
このため、3 つめの「プログラムを受け取り、実行し、結果を得る」という部分を GitHub Actions(と ChatGPT)で作成することにしました。
まずはハードに
問題は、問題セットに依らないで、一意に決定できると仮定します。
つまり、各問題は PRB001_A などと命名されているものとします。
problems には、問題データ(人が読む文章や、テストケースとその模範解答)を配置します。
submissions には、コンテスタントが解いた答案(現状 Python のみ)を配置します。
開発初期は on: workflow_dispatch に引数を渡すことでのみ発火するようにしていました。
答案を 1 つだけ受け取って、ジャッジする流れです。
そして柔軟に
problems/**/ には specification.json という説明書きを配置しました。
問題の ID、CPU の時間制限、テストケース名が記されています。
{
"id": "PRB001_A",
"cpu_limit_seconds": 2,
"cases": [
"sample/hand_00",
"sample/hand_01"
]
}
時間計測やその他処理は、最初は GitHub Actions を Ubuntsu で走らせていることを理由に Bash を用いていました(judge.sh)。
途中で、Python の実行環境をセットアップしているついでに、メイン処理も Python で動かすように変更しました(judge_runner.py)。
ジャッジ基準
TLE (Time Limit Exceeded) は、CPU 時間を 2 秒使うか、現実世界で 2+5 = 7 秒経過するか、で判断しています。
sleep 系の関数を使うと、CPU 時間を使わずに処理が進められると思ったため、二重に測定しています。
MLE (Memory Limit Exceeded) は、メモリを制限する方法が思いつかなかったので無いです。
RE (Rumtime Error) は、子となる Python プロセスが異常終了のエラーコードを吐くことを基準にしています。
WA (Wrong Answer) は、テストケースに紐づいた模範出力との完全一致で確かめています。
このため、まだ小数は扱えないです。
できたもの
ディレクトリ構成
root/
|- .github/workflows/
| |- judge.yml
|
|- judgesys/
| |- judge_runner.py
|
|- problems/
| |- PRB001_A/
| | |- README.md
| | |- specification.json
| | |- cases/sample
| | |- hand_00.txt
| | |- hand_00_out.txt
| | |- ...
| |- ...
|
|- submissions/
|- PRB001_A/
| |- answer.py
| |- ...
|- ...
Judge.yml
ほとんどコードエージェントに書いてもらったので、詳しい話はできませんが、流れだけ。
- Pull Request を提出箱とし、オープン時・ファイル更新時・再オープン時に、
submissions/**/に Python ファイルが追加されているなら Action を発火する - 答案ファイルの path を収集する
- 動的に Job を作成できる Matrix を使い、それぞれの答案ファイルに対して、メイン処理
judge_runner.pyを実行する - 答案ファイルのパスから問題 ID を取得し、
specifcation.jsonを読む - 各テストケースに対し、実行して結果を得る
-
::error::を用いて出力し、通っていないケースを公開する
具体的な実行結果は以下にあります。
さいごに
なんとまぁ、AI と共に粗雑ながらジャッジ環境を作れてしまいました。
もっと見やすく、もっと扱いやすく、という展望はありますが、今回はこの辺りで。
MMA Contest 021 もぜひどうぞ!
また、MMA Advent Calender 2025 の他の記事もごひいきに。