# Introduction
> Contributed by <[Jackiempty](https://github.com/Jackiempty)>
這份文件目的在於紀錄我在架設 GitHub Classroom 的一切過程,首先可以作為我將來要復現這次的成果的參考資料,再來可以成為將來學弟妹要學習時可以參考的筆記
# Content
## 怎麼架 Organization
要開啟能夠容納一門課程所有內容的平台要做的第一件事就是先創建這個課程專屬的 GitHub Organization。
在這個 Organization 中,你可以將作業模板的 Repo、作業中會用到的客製化 Submodule、運行作業的虛擬環境 image,都可以在這裡保存,甚至連學員的每一份作業的撰寫過程的 commit 也會保留,所以最重要的就是你要怎麼去維護這些智慧財產,你有用心維護,這個平台上面的內容就可以一直使用下去,你不斷迭代的過程也得以保存。
### 創建 Organizaion
這部分的步驟不詳細贅述,因為網路查一查就有了,這邊比較需要注意的點是在創建時的幾個原則:
#### 1. 命名方式
目前我所接觸過的兩個課程用 Organization 分別是**計算機組織-2025**以及**人工智慧晶片設計與應用-2026**,之前是峻豪學長先為計算機組織的 Organization 命名之後我延續他的格式命的名,若後續有人有更好的想法也可以提出。
目前的命名方法是這樣:
`<課程英文名稱>-at-NCKU-EE`
所以按照前面的例子的話就會是:
`Computer-Organization-at-NCKU-EE`,
`AI-on-Chip-at-NCKU-EE`, etc.
這樣的命名方式是建立在同一個課程 Organization 會重複使用好幾年的情況,因為想說如果新的一年就要就一個新的 Organization 在管理上會比較麻煩一點,因此目前先暫時採取預設會重複使用的情境。
#### 2. 成員邀請
這邊會作為正式成員加入的通常就是這門課的助教們,我是會直接寄邀請連結,並且權限都設定成 Owner,除非有要很明確地區分大助跟小助的權限,不然就直接設成 Owner 在調整一些設定上就不用有溝通的麻煩
#### 3. 歡迎補充
To be contributed
### Organization 可以存放的內容
#### 1. 每個 Lab 的作業模板
目前已經完成的案例有計算機組織的 `Lab 1` ~ `Lab 4` 和 AoC 的 `Lab 4`,目的就是將原本都使用 Moodle 提供壓縮包的做法替換成直接從作業模板 fork 給每一個學員,這樣就不用去煩惱 Moodle 權限或是時效的問題,同一個 Lab 也可以不斷紀錄迭代的過程
#### 2. Lab 的運行虛擬環境
除了 Repo 外 Organization 也可以存放 Docker image,所以你也可以將你預計要可以兼容所有 Lab 運行環境的 Docker image 存放到 Organization,同時做好版本控制
#### 3. Lab 可能用到的 Submodule
有時候我們會寫一些小工具庫當作 Submodule 引入作業中使用,這時候也可以直接將這些工具庫包裝成 Repo 存到 Organization 裡面
### 設定 Organization
這裡有些設定是跟 Runner 權限有關的,你可以先看過去,等看完後面 Runner 的內容你就知道這裡在幹嘛了
#### Runner Group 權限
我剛架好 Runner 有遇到一個問題,那就是我 push 上去的 commit action 並沒有被符合條件的 Runner picked up 並執行,這時候需要去到 Org Settings > Actions > Runner groups,將 Repository access 改為 All repositories。或是如果你將 Assignment 的 Repository visibility 設為 Public 的話,也要把 Runner groups 底下的 `Allow public repositories` 勾選起來。
## 架設 Runner
完整的架設 Runner 的教學官方有文件可以參考,我直接附在[這裡](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners#adding-a-self-hosted-runner-to-an-organization),我這邊簡短說明整個過程。
### 創建 Runner
以下是你選用 Linux 創建新的 Runner 時要執行的命令:
```bash
# Create a folder
$ mkdir actions-runner && cd actions-runner
# Download the latest runner package
$ curl -o actions-runner-linux-x64-2.331.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.331.0/actions-runner-linux-x64-2.331.0.tar.gz
# Optional: Validate the hash
$ echo "5fcc01bd546ba5c3f1291c2803658ebd3cedb3836489eda3be357d41bfcf28a7 actions-runner-linux-x64-2.331.0.tar.gz" | shasum -a 256 -c
# Extract the installer
$ tar xzf ./actions-runner-linux-x64-2.331.0.tar.gz
```
> 下載壓縮包,進行驗證並解壓縮
```bash
# Create the runner and start the configuration experience
$ ./config.sh --url https://github.com/<Your course>-at-NCKU-EE --token <Your Token>
# Last step, run it!
$ ./run.sh
```
> 進行設置並運行
基本上你要做的就是下載官方的 Runner 安裝包,解壓縮,用網站提供給你的 Token 進行設定,你就完成了一個可用的 Runner 了。
```
aislab@aislabcourse:~/workspace/github-self-hosted-runners-AoC$ ll actions-runner-1
total 60
drwxr-xr-x. 4 aislab aislab 12288 Jan 28 17:30 bin
-rwxr-xr-x. 1 aislab aislab 2458 Jan 10 01:22 config.sh
drwxr-xr-x. 4 aislab aislab 4096 Jan 29 20:45 _diag
-rwxr-xr-x. 1 aislab aislab 646 Jan 10 01:22 env.sh
drwxr-xr-x. 6 aislab aislab 76 Jan 10 01:23 externals
-rw-r--r--. 1 aislab aislab 1619 Jan 10 01:22 run-helper.cmd.template
-rwxr-xr-x. 1 aislab aislab 2663 Jan 28 16:46 run-helper.sh
-rwxr-xr-x. 1 aislab aislab 2663 Jan 10 01:22 run-helper.sh.template
-rwxr-xr-x. 1 aislab aislab 2535 Jan 10 01:22 run.sh
-rwxr-xr-x. 1 aislab aislab 532 Jan 28 17:30 runsvc.sh
-rwxr-xr-x. 1 aislab aislab 751 Jan 10 01:22 safe_sleep.sh
-rwxr-xr-x. 1 aislab aislab 5234 Jan 28 16:45 svc.sh
drwxr-xr-x. 7 aislab aislab 97 Jan 29 20:45 _work
```
這邊要特別說明的是我們架設 Runner 的方式,由於我們的 Runner 會給整個班級將近 100 人去使用,因此我們會需要不只一個 Runner,這也是為什麼要自己架的原因之一,因為官方提供的 Runner 比較偏向是個人開發用,一個帳號/組織只會有一個,如果我們整個班級的 Action 都依賴這個 Runner 去跑的話,在交作業前夕可能就會塞爆唯一的這個 Runner,因此我們特地多架了幾個以應付尖峰時刻的需求,這邊要來說明如果是要架好幾個 Runner 可以採取的做法。
首先是解壓縮的部分,因為只要是同一個版本的 Runner 他的安裝壓縮包都是一樣的,所以我大可直接用同一個壓縮包分別解壓縮到好幾個不同的資料夾,每個資料夾乘載著各自的 Runner 的資料。原本的那行解壓縮的命令會直接將壓縮包的內容解壓縮在當前的 Directory,所以需要將那行命令改成 `$ tar xzf ./actions-runner-linux-x64-2.331.0.tar.gz -C ./<runner folders>`,就可以將解壓縮的內容導入當前檔案目錄底下的其他資料夾,最後看起來就會像這樣:
```
aislab@aislabcourse:~/workspace/github-self-hosted-runners-AoC$ ll
total 217276
drwxr-xr-x. 6 aislab aislab 4096 Jan 30 03:35 actions-runner-1
drwxr-xr-x. 6 aislab aislab 4096 Jan 30 03:35 actions-runner-2
drwxr-xr-x. 6 aislab aislab 4096 Jan 30 03:35 actions-runner-3
drwxr-xr-x. 5 aislab aislab 4096 Jan 30 03:35 actions-runner-4
drwxr-xr-x. 5 aislab aislab 4096 Jan 30 03:35 actions-runner-5
drwxr-xr-x. 6 aislab aislab 4096 Jan 30 03:35 actions-runner-6
drwxr-xr-x. 6 aislab aislab 4096 Jan 30 03:35 actions-runner-7
drwxr-xr-x. 6 aislab aislab 4096 Jan 30 03:35 actions-runner-8
-rw-r--r--. 1 aislab aislab 222457434 Jan 28 16:25 actions-runner-linux-x64-2.331.0.tar.gz
```
透過每個資料夾在執行 `./config.sh` 的時候使用新的 Token 就可以創建多個 Runner 了。
### Runner Service
根據上面的架設過程中的最後一個步驟是使用 `./run.sh` 令 Runner 跑起來,但現在這個跑起來的狀態是目前這個 Session 運行在目前這個 Terminal,如果你 `Ctrl + C` 或是關閉這個 Terminal 就會結束這個 Session,導致 Runner 下線,因此我們需要將這個 Session 用作一個 Service 讓它運行在後台。
好在 Runner 安裝的壓縮包裡面也有提供這個功能,詳情可以看[這裡](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/configure-the-application?platform=linux)。
簡單來說你只要執行這幾行命令就可以讓當前資料夾的 Runner 作為一個 Service 在後台運行:
```bash
# 安裝服務
sudo ./svc.sh install
# 啟動服務
sudo ./svc.sh start
# 檢查狀態
sudo ./svc.sh status
```
### SELinux 權限阻擋 (最常見於 RedHat/CentOS/Fedora)
我目前運行 Github Runner 的電腦是實驗室的某台閒置的個人電腦,上面灌的系統是 RedHat 的 Rocky Linux,然後就遇到了這個問題。
解決方法我是問 Gemini 的,我直接把它那套解決流程貼上來:
-----
如果你的學校伺服器有啟用 SELinux,它通常會禁止 systemd 服務去執行 /home 目錄下的二進制檔案(因為這被視為潛在的安全風險)。
1. 檢查與解決:
```bash
$ sestatus
```
如果顯示 `Current mode: enforcing`,那兇手就是它。
2. 暫時解決方案
```bash
$ sudo setenforce 0
$ sudo ./svc.sh start
```
如果這樣就成功了,代表你需要修正檔案的 SELinux 標籤,而不是永久關閉防火牆。
3. 永久修正方案(還原 enforcing 並修正標籤)
```bash
$ sudo setenforce 1
# 告訴 SELinux 允許 bit_t 類型的程式在 home 執行(或是直接標記該資料夾)
$ sudo chcon -R -t bin_t /home/
# 再次啟動
$ sudo ./svc.sh start
```
## 將 Docker image 上傳到 Organization Package
將環境打包成 Docker Image 並放在 GitHub Packages (GHCR),可以確保所有學生的作業都在**完全一致**的環境中評分,且不會污染你的實體 Runner。
### 前情提要
這邊先說明當前環境要先有什麼樣的條件,才可以順利進行接下來的步驟。
1. 當前的主機要有安裝 Docker
2. 當前的主機上的使用者需要被加進 Docker 群組(才可以不用 sudo 就操作 Docker)
### 第一階段:準備存取權杖 (PAT)
要將 Docker Image 推送到 Organization,你需要一個有權限的 Token(密碼是推不上去的)。
1. 點擊 GitHub 右上角頭貼 -> **Settings** -> **Developer settings** -> **Personal access tokens** -> **Tokens (classic)**。
2. **Generate new token (classic)**。
3. **Scopes (權限) 必選:**
* `write:packages` (上傳 image 用)
* `read:packages` (下載 image 用)
* `delete:packages` (管理用)
* `repo` (通常建議勾選,以免讀取不到某些 repo 資訊)
4. 複製這個 Token (以 `ghp_` 開頭)。
### 第二階段:製作並推送 Image
我的案例中的 Organization 名稱是 `AI-on-Chip-at-NCKU-EE`,並且我將 Image 命名為 `aoc-docker-env`。
在你的開發電腦上(有 Dockerfile 的地方):
#### 1. 登入 GitHub Container Registry (GHCR)
```bash
# 在終端機執行,密碼請貼上剛剛申請的 PAT
docker login ghcr.io -u <你的GitHub帳號>
```
#### 2. Build Image
```bash
# 注意:GitHub 規定 Image 名稱必須全部小寫
docker build -t ghcr.io/ai-on-chip-at-ncku-ee/aoc-docker-env:v1 .
```
#### 3. Push Image
```bash
docker push ghcr.io/ai-on-chip-at-ncku-ee/aoc-docker-env:v1
```
### 第三階段:設定 Image 權限
預設情況下,推送到 Org 的 Image 可能是 **Private** 的。為了讓 GitHub Actions (即使是學生的 repo) 能順利拉取,最簡單的方式是將其設為 **Public** (如果是教學基礎環境),或是確保 Workflow 有權限存取。
1. 進入 Organization 首頁 -> **Packages** 分頁。
2. 點選剛上傳的 `aoc-docker-env`。
3. 右側點選 **Package Settings**。
4. 找到 **Change visibility**,將其改為 **Public** (建議)。
*為什麼?* 如果設為 Private,你必須在每個學生的 Repo 裡設定 Secret 才能拉取 Image,這在 Classroom 場景下管理會很痛苦。只要 Image 裡沒有標準答案或敏感金鑰,公開環境是安全的。
## 運營 Classroom
這部分會包辦從創建 Classroom 到創建 Assignment 的一些過程跟細節
### 創建新的 Classroom
當你啟用你帳號的 Classroom 功能之後,你就可以看到 Your Classrooms 的頁面,如果你已經是某個 Classroom 的 Admins 那你就會看到已經有 Classrooms 了,或是你只會看到一個 `+` 號,點進去就可以開始創建 Classroom 了。

這裡只講兩件事,Organization 的選擇和 Classroom 的命名:
#### Organization 的選擇
`+` 號點進去之後你會先看到要使用的是哪個 Organization,就選那個你為了這門課而創立的 Organization 就好了,至於創建 Organization 的過程請參閱[上方](https://hackmd.io/@Jackiempty/r1SemSPIWe#%E6%80%8E%E9%BA%BC%E6%9E%B6-Organization)

#### 命名
目前的兩個案例也還是 2025 年的計算機組織跟今年 2026 年的 AoC,目前採取的命名原則是`<課程英文> + <季度> + <年份>`,目前的例子來看的話就是:
`CompOrg Fall 2025`,
`AoC Spring 2026`, etc.
假設下個學年 EAI 也要開設 Classroom 平台的話或許就會是:
`EAI Fall 2026`
### 邀請成員
現在你已經有一個 Classroom 了,這個 Classroom 會有兩種成員,TA/Admins 跟 Students。
Admins 的部分你可以用連結邀請的方式將他們加入 Admins,這裡要注意的一個前提是,他們首先要先是 Organization 的 Owner 才可以是 Classroom 的 Admins,所以前面 Organization 的成員邀請的步驟也別漏掉了。
Students 的部分比較複雜一點,我先來解釋 Students 在這個 Classroom 裡面的定位是什麼。當我們在用邀請連結派發 Assignments 時,接受邀請並和作業的 Repo 綁定的單位其實是學員們的個人 GitHub 帳號,但因為 GitHub 帳號有匿名性,你在監測作業進度或是狀況的時候如果沒有這個 GitHub 帳號對應的真人資料,會很難管理每個具有匿名性的 GitHub 帳號和他們的成績。因此我們會預先根據選課系統或是 Moodle 的資料先建立課堂的成員名單,並將名單整理成 CSV 或 TXT 後匯進 Classroom 的 Students 中,並讓學員在接受作業邀請的當下就會去認領自己的真人資料,這樣就會建立個人帳號跟課堂學員個人資料之間的連結,讓我們在登記成績或是排解問題的過程更為方便。
這邊簡單說明一下 Students 這邊的操作介面
----

從這個頁面可以看到幾個要素,首先在 All students 這邊可以看到有兩種人,一個是上面有學員資訊,下方有藍色字的個人 GitHub 帳號,這個是已經認領完學員資料的 GitHub 帳號會顯示的樣子,在下方有兩個預設頭像的學員資訊,他下面顯示 `Unlinked user`,代表說這個學員資訊尚未被這個人的個人 GitHub 帳號認領,右邊也可以看到若本人沒認領 Admins 也有權限去手動 Link/Unlink。
----

再來我們看到 Unlinked GitHub accounts 的頁面,跟另外一邊反過來,這邊顯示的是已經接受作業邀請但沒有認領學員資料的 GitHub 帳號,右邊也有 Link to student 可以操作。
----

最後我們看看如果你要新增學員的話要怎麼做。點進 Students 頁面右上方的 Update students,就可以進到新增學員的視窗,這邊我不討論上方那個使用 Google Classroom 同步的功能,因為我沒有用到,我只說明下面的 Create your roaster manually 跟 Upload a CSV or txt file,Create your roaster manually 就是手打資料,如果後續有加簽或是旁聽的同學就可以用這個方式另外加入,Upload a CSV or txt file 就可以直接將整個班級的資料匯入,這部分我就不贅述了。
----
到這邊 Students 的運作機制應該說明的差不多了。
### 創建新的 Assignment
進到某個 Classroom 以後,就可以開始操作 Assignment 了。
----

第一個頁面是在
1. 取作業名稱
2. 設繳交期限
3. 個人或小組作業
作業名稱應該就都是什麼 Lab-x 這樣,看你那個學期有幾個 Lab,當然如果你要根據作業內容去取我也不反對,但個人更喜歡統一格式,這樣既可以按照出的順序排序,也一目瞭然。
繳交期限決定完之後還可以設定可不可以遲交,意思是在期限過後學員還可不可以 push 新的 commit 上去,這個也是看各自需求囉。
至於個人作業或小組作業的差別應該是在區分同一個 Repo 是否是多人協作,個人作業就會是從模板 fork 新的 Private Repo 到 Organization 底下在名稱註記每個人的 GitHub 帳號。
----

第二個頁面是在
1. 選取要使用的模板
2. 設定 Repository visibility
3. 設定 Online IDE
模板可以是廣大的 GitHub 所有 Repos 中任何一個 Public 的 Repo,但我們這邊就是用我們事先寫好放在 Organization 的 Repo 當作模板。
Repository visibility 這邊建議一率設定成 Private,因為如果是 Public 的話學員之間就可以互相看到彼此的作業答案了。
然後 Online IDE 就設成 No online IDE 就可以了。
----

最後是要設定 autograding 的腳本,這邊就略過就好了,我們要採用的不是這邊要寫的,是我們自己事先寫好在 Repo 裡面的,如果你又在這邊寫的話反而會覆蓋掉你自己事先寫好在 Repo 裡面的,所以這邊就是建議不要動就好了。
----
至於 autograding 腳本的操作會在等下提到。
### 派發 Assignment
當上述所有步驟都準備完畢之後,就可以將作業的邀請連結複製下來公告到 Moodle 上的作業公告了,是不是很簡單呢!
## 怎麼寫 classroom.yml
寫到這邊我已經好累了,所以我這邊打算偷懶一點。
先來介紹 classroom.yml 的功用,前面我們鋪陳那麼多環境,就是為了要讓這個名為 classroom.yml 的腳本可以做出我們想要他做的事情,說到腳本,其實他也不一定要叫 classroom.yml 才可以被 GitHub Action 執行,你只要是放在 Repo 底下的 `.github/workflow/*.yml` 在 commit 被 push 到 remote 的時候都會自動進到 Action 裡面去跑裡面的 `*.yml`,這裡要將其命名為 classroom.yml 的原因我猜是要這樣才可以跟 Classroom 連動,不然這個腳本就只會被當作一般個 Action 在 Runner 跑完就沒了,Classroom 的介面不會顯示測試結果。
### 撰寫 classroom.yml
那至於 classroom.yml 要怎麼寫我也懶得介紹了,我直接放模板上來讓大家參考,你可以拿去問 AI,或是你想要寫自己的腳本的時候就拿這個模板去改就好了。
```yaml
name: Autograding Tests
on:
- push
- workflow_dispatch
- repository_dispatch
permissions:
checks: write
actions: read
contents: read
jobs:
run-autograding-tests:
# --- [設定 1] 執行環境選擇 ---
# 雲端執行器使用 ubuntu-latest,若有特殊硬體需求則改用 self-hosted
runs-on: ubuntu-latest
# --- [設定 2] Docker 容器環境 (若不需要可整段刪除) ---
container:
image: <YOUR_DOCKER_IMAGE_PATH> # 例如: ghcr.io/username/repo:tag
options: --user root
# 避免機器人帳號觸發迴圈
if: github.actor != 'github-classroom[bot]'
steps:
- name: Checkout code
uses: actions/checkout@v4
# --- [設定 3] 編譯與環境準備階段 ---
- name: Preparation and Build
run: |
echo "Preparing environment and building project..."
# 在此輸入你的編譯指令,例如:
# make all
# chmod +x ./bin/*
<YOUR_BUILD_COMMANDS>
# --- [設定 4] 測試項目 (按需複製多個項目) ---
# 測試項目 A
- name: "<TEST_NAME_A>"
id: test-id-a # 記住這個 ID,最後彙整報告會用到
uses: classroom-resources/autograding-command-grader@v1
with:
test-name: "<DISPLAY_NAME_A>"
command: "<EXECUTION_COMMAND_A>" # 執行的腳本或程式路徑
timeout: 5 # 分鐘
max-score: 10 # 該項分數
# 測試項目 B
- name: "<TEST_NAME_B>"
id: test-id-b
uses: classroom-resources/autograding-command-grader@v1
with:
test-name: "<DISPLAY_NAME_B>"
command: "<EXECUTION_COMMAND_B>"
timeout: 5
max-score: 10
# --- [設定 5] 自動評分結果彙整器 ---
- name: Autograding Reporter
uses: classroom-resources/autograding-grading-reporter@v1
env:
# 將上面的 ID 與結果連結 (格式: <ANY_NAME>_RESULTS)
TEST-ID-A_RESULTS: "${{steps.test-id-a.outputs.result}}"
TEST-ID-B_RESULTS: "${{steps.test-id-b.outputs.result}}"
with:
# 這裡必須對應上面所有測試項目的 ID,用逗號隔開
runners: test-id-a,test-id-b
```
### 模板使用說明與建議
這裡歸納了幾個重點:
#### 1. 執行器與容器 (`runs-on` & `container`)
* 如果你是做一般程式作業(C/C++, Python, Java),建議直接用 `ubuntu-latest`。
* 如果你需要特定的編譯鏈(如你原始檔案中的 **DLA** 或 **AI-on-Chip** 環境),請填入對應的 Docker Image。
* 如果是 private repository,並且是大量的 Action 需求(例如大量課程作業評分),強烈建議使用自架 runner,因為使用官方提供的 `ubuntu-latest` 會消耗 quota,如果當月免費額度用完,則有設定付款資訊可能會被開始計費,沒設定的話則會終止服務;但如果是 public repository 則無此限制。
#### 2. 測試項目的增減
每一項測試都由 `autograding-command-grader` 組成。如果你有 5 個測項,就複製 5 次。
* **關鍵連動**:`steps.<ID>.outputs.result` 必須在最後的 `Autograding Reporter` 步驟中被列出,否則 GitHub Classroom 的分數板不會更新。
#### 3. 常見的 `command` 寫法
* **直接執行**:`./my_test_bin`
* **Python 腳本**:`python3 tests/grade_script.py`
* **跨目錄執行**:`cd test/folder && ./run_test.sh`(如你原始檔案中的 DLA 寫法)
#### 4. 權限設定 (Permissions)
模板中保留了 `checks: write`,這是為了讓 GitHub Classroom 能在 Pull Request 或 Commit 介面直接顯示「打勾」或「打叉」的圖示,請務必保留。
另外為了省下 image 內部的預設 User 的權限問題,建議一率在腳本裡面加上
```yaml
container:
image: <YOUR_DOCKER_IMAGE_PATH> # 例如: ghcr.io/username/repo:tag
# 加上這行 ↓
options: --user root
```
將 container 要跑的時候設成 root。
### 小插曲 - 1
這邊分享一個我在研究 autograding 系統的時候走的一些彎路。
原本 AI 跟我說可以用一個叫做 `education/autograding@v1` 的工具就可以將評分項目的細項外包出去 `.github/classroom/autograding.json`,這樣在文本上的可讀性會比較好。但後來發生了一個問題,那就是用這個方式評出來的分數沒辦法回傳回去 Classroom 顯示在那邊的介面上,後來發現好像 `education/autograding@v1` 以及 `education` 系列的工具已經停止支援了,所以雖然他能幫你算分數但已經沒辦法連動 Classroom 了。
結果我最後又捨棄了寫 `autograding.json` 的方法回到全部都寫在 `classroom.yml` 並使用 `classroom-resources/autograding-command-grader@v1` 跟 `classroom-resources/autograding-grading-reporter@v1` 做為算分數跟傳分數的工具,也就形成了上面的那個模板。
### 小插曲 - 2
這邊分享一個我在研究去年計算機組織的 Lab 的 autograding 時發現的一個問題。
簡單來說就是 GitHub Action 在判定一個測試有沒有通過的標準在於它用於測試的那行命令的 Exit Code 是不是 0,在這樣的標準下我目前觀察到有三種情況會造成「假陽性」(False Positive)的結果發生。「假陽性」的意思是學生做錯了,測試沒有通過,但由於該命令下達的方式讓測試即使沒通過,程式的 Exit Code 還是 0,造成即使測試沒通過,在 GitHub Action 的判定機制會判定成通過,進而誤判分數。
#### 1. Compilation 失敗導致 ctest 找不到測試 (No tests found)
- 問題描述:
當學生的程式碼編譯失敗(或 CMake 設定錯誤),導致測試執行檔沒有產生。執行 ctest 時,系統回報 No tests were found,但該步驟卻被判定為 Success,學生獲得滿分。
- 發生原因:
ctest 的預設行為認為「執行過程無報錯」即為成功。雖然它沒找到任何測試案例,但它成功地完成了「搜尋測試」這個動作,因此回傳 Exit Code 0。
- 解決方法:
強制 ctest 在找不到測試案例時視為錯誤。使用 --no-tests=error 參數。
```yaml
# 在 classroom.yml 中
- name: Run Test
uses: classroom-resources/autograding-command-grader@v1
with:
# 加上 --no-tests=error
command: "cd build && ctest -R 'TargetTest' --no-tests=error"
```
#### 2. Shell / Makefile 迴圈中的錯誤被吞噬 (The Loop Trap)
- 問題描述:
使用 for 迴圈連續跑多個測試案例(例如用 Shell script 或在 Makefile recipe 中)。前面的 test1 失敗了,但因為最後一個 test3 成功,整段 Script 被判定為成功。
- 發生原因:
在 Shell (bash/sh) 中,一個區塊(如 for loop)的 Exit Code 預設取決於 「最後一個被執行的指令」。中間的失敗只會印出錯誤訊息,但不會中斷迴圈,導致 Exit Code 被最後一次成功的執行覆蓋為 0。
- 錯誤範例(Makefile)
```makefile
test:
# 若 test1 失敗,test2 成功,Make 會收到 0 (成功)
for exe in ./test1 ./test2; do \
$$exe; \
done
```
- 解決方法
使用 set -e 強制 Shell 在遇到任何錯誤時立即退出 (Fail Fast)。
```makefile
test:
# set -e 確保只要這行裡有任何指令失敗,立刻中止並回傳錯誤
set -e; \
for exe in ./test1 ./test2; do \
$$exe; \
done
```
或者針對單行指令做例外處理:`$$exe || exit 1;`
#### 3. 錯誤狀態碼被「清理指令」洗白 (The Status Wash Trap)
- 問題描述
在 Makefile 裡面跑迴圈測試,進入子目錄執行測試後,為了讓迴圈能繼續跑,習慣性地加上 cd - 回到上一層目錄。 結果:就算測試全部失敗,Autograding 依然判定滿分。
- 發生原因
這是 「指令分隔符」 與 「Exit Code 覆蓋」 的雙重災難。
觀察這段有問題的程式碼:
```makefile
test:
for case in $(CASES); do \
cd $$case && $(MAKE) test; \ # <-- 若這裡失敗 (Exit 2)
cd -; \ # <-- 分隔符號讓 Shell 繼續執行這行
done
```
1. 分隔符號 (`;` 或換行):Shell 的分號代表「執行完前一個指令,不管成功失敗,接著執行下一個」。
2. 洗白過程:
- `$(MAKE) test` 失敗,產生 Error Code。
- 但因為分號的存在,Shell 接著執行 `cd -`。
- `cd -` (回到上一頁) 這個動作通常是成功的 (Exit Code 0)。
3. 最終結果:這一輪迴圈的「最後一個指令」變成了 `cd -`。對 Shell 來說,這一輪是成功的。Make 收到 0,判定 Pass。
- 錯誤範例
```makefile
test:
# 這裡的 cd - 會把上一行 make 的錯誤掩蓋掉
for case in case1 case2; do \
cd $$case && $(MAKE) test; \
cd -; \
done
```
- 解決方法
- 方法 A:使用 `make -C` 避免切換目錄
`make` 提供了 `-C` 參數,允許你在不實際切換 Shell 當前目錄的情況下,去執行別的目錄裡的 Makefile。這樣就完全不需要寫 `cd -`,也就不會有狀態碼被洗白的問題。 同時,務必配合 `set -e` 確保發生錯誤時立即中止。
```makefile
test:
# set -e: 確保任何指令失敗就立刻退出,不會繼續跑迴圈
# -C: 告訴 make 去該目錄執行,不需要手動 cd
set -e; \
for case in $(CASES); do \
$(MAKE) -C $$case test; \
done
```
- 方法 B:嚴格的錯誤捕捉
如果你必須使用 `cd`,則必須確保在 `make` 失敗的當下,整個 Script 就該爆炸退出,而不是繼續執行 `cd -`。
```makefile
test:
for case in $(CASES); do \
# 使用 || exit 1 強制在 make 失敗時退出 Shell
cd $$case && $(MAKE) test || exit 1; \
cd -; \
done
```
:::info
Disclaimer:
這是我真實遇到的問題,但我是將問題描述之後交給 AI 統整成筆記內容,在我自己看過之後覺得邏輯完整度還算 OK,因為這幾個案例有些是複合條件的,所以要不太可能完全列舉所有可能的情況。
所以要是你有發生類似問題但都無法精準對應到上述問題描述的話,歡迎補充 (contribute)~
:::
## Reference
- [停止支援的 `education/autograding@1`](https://github.com/github-education-resources/autograding)
- [他們的後繼工具 `classroom-resources`](https://github.com/classroom-resources)
- [Adding self-hosted runners](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners)
- [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/configure-the-application?platform=linux)
- [Working with the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
# Epilogue
本文件詳盡記錄了 GitHub Classroom 在 NCKU-EE 課程中的應用實務,涵蓋了 Organization 規範、Self-hosted Runner 群組架設、Docker 容器化環境以及 Autograding 腳本的標準化。
建立這套流程的核心目的在於達成「環境一致性」與「評分自動化」。隨著 GitHub 功能的更新,建議後續維護者定期檢視 classroom-resources 的最新版本。如有任何配置上的疑問,可參考文中附上的官方文件連結或與相關實驗室成員討論。