Skip to content

Commit 00006e4

Browse files
committed
Add GitHub Actions CI for build, lint, and testing
This introduce a CI pipeline: - commit-hygiene: SHA-1 prefix, subject format, AI trailer rejection - lint: clang-format, trailing newlines, banned func, cppcheck, shfmt - unit-tests: test-game and test-coro - build: kernel module and xo-user compile check - integration-tests: insmod, device I/O, game completion, kernel health, clean unload with dmesg diagnosis
1 parent 00008d0 commit 00006e4

13 files changed

Lines changed: 631 additions & 15 deletions

File tree

.ci/check-banned.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Detect banned unsafe functions in C source (excluding test files).
4+
# Matches the pre-commit hook's banned list: gets, sprintf, strcpy.
5+
6+
set -e
7+
8+
ret=0
9+
10+
while IFS= read -r -d '' f; do
11+
# Use word-boundary-aware patterns:
12+
# \bgets\s*\( -- catches gets( at line start; exclude fgets via grep -v
13+
# \bsprintf\s*\( -- unbounded sprintf
14+
# \bstrcpy\s*\( -- unsafe strcpy
15+
output=$(grep -nE '\b(sprintf|strcpy)\s*\(' "$f" || true)
16+
gets_output=$(grep -nE '\bgets\s*\(' "$f" | grep -v 'fgets' || true)
17+
combined="${output}${gets_output}"
18+
if [ -n "$combined" ]; then
19+
echo "Banned function in $f:"
20+
[ -n "$output" ] && echo "$output"
21+
[ -n "$gets_output" ] && echo "$gets_output"
22+
echo ""
23+
ret=1
24+
fi
25+
done < <(git ls-files -z 'src/*.c' 'user/*.c' 'include/*.h')
26+
27+
if [ $ret -eq 0 ]; then
28+
echo "No banned functions found."
29+
fi
30+
exit $ret

.ci/check-cppcheck.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Run cppcheck static analysis via the project's canonical script.
4+
# Wraps scripts/cppcheck.sh with a timeout to prevent CI hangs.
5+
6+
set -e
7+
8+
timeout 120 scripts/cppcheck.sh

.ci/check-format.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Validate C/H files against .clang-format using clang-format-20.
4+
# Exit 0 if clean, 1 if violations found.
5+
6+
set -e
7+
8+
if ! CLANG_FORMAT=$(command -v clang-format-20); then
9+
echo "[!] clang-format-20 not installed." >&2
10+
exit 1
11+
fi
12+
13+
ret=0
14+
while IFS= read -r -d '' f; do
15+
diff=$(diff -u "$f" <("$CLANG_FORMAT" "$f") || true)
16+
if [ -n "$diff" ]; then
17+
echo "Format violation: $f"
18+
echo "$diff"
19+
echo ""
20+
ret=1
21+
fi
22+
done < <(git ls-files -z '*.c' '*.h')
23+
24+
if [ $ret -eq 0 ]; then
25+
echo "All files pass clang-format-20."
26+
fi
27+
exit $ret

.ci/check-newline.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Ensure all tracked C/H files end with a newline.
4+
5+
set -e
6+
7+
ret=0
8+
while IFS= read -r -d '' f; do
9+
[ -s "$f" ] || continue
10+
if [ "$(tail -c 1 "$f" | wc -l)" -eq 0 ]; then
11+
echo "Missing final newline: $f"
12+
ret=1
13+
fi
14+
done < <(git ls-files -z '*.c' '*.h')
15+
16+
if [ $ret -eq 0 ]; then
17+
echo "All files end with newline."
18+
fi
19+
exit $ret

.ci/check-shell.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Validate shell scripts with shfmt (per .editorconfig settings).
4+
5+
set -e
6+
7+
if ! SHFMT=$(command -v shfmt); then
8+
echo "[!] shfmt not installed." >&2
9+
exit 1
10+
fi
11+
12+
ret=0
13+
while IFS= read -r -d '' f; do
14+
[ -f "$f" ] || continue
15+
if ! "$SHFMT" -d "$f" > /dev/null 2>&1; then
16+
echo "Shell format violation: $f"
17+
"$SHFMT" -d "$f" || true
18+
echo ""
19+
ret=1
20+
fi
21+
done < <(git ls-files -z '*.sh' '*.hook' 'scripts/install-git-hooks')
22+
23+
if [ $ret -eq 0 ]; then
24+
echo "All shell scripts pass shfmt."
25+
fi
26+
exit $ret

.github/workflows/main.yml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Build kxo kernel module and run full test suite.
2+
#
3+
# Parallelism (4 independent jobs, 1 sequential):
4+
# commit-hygiene -- vanity hash prefix + subject format + AI trailer detection
5+
# lint -- clang-format, newline, banned functions, cppcheck, shfmt
6+
# unit-tests -- test-game, test-coro (no kernel dependency)
7+
# build -- kernel module + xo-user compile check
8+
# integration -- same-runner build, insmod, device I/O, game completion,
9+
# graceful stop via sysfs, clean unload
10+
#
11+
# commit-hygiene, lint, unit-tests, and build run in parallel.
12+
# integration-tests waits for build only.
13+
name: Build and Test
14+
15+
on:
16+
push:
17+
branches: [main, master]
18+
pull_request:
19+
branches: [main, master]
20+
21+
jobs:
22+
# ---- Commit hygiene: vanity hash prefix + subject format ----
23+
commit-hygiene:
24+
runs-on: ubuntu-24.04
25+
steps:
26+
- name: Checkout (full history for commit validation)
27+
uses: actions/checkout@v6
28+
with:
29+
fetch-depth: 0
30+
31+
- name: Validate commit log
32+
env:
33+
EVENT_NAME: ${{ github.event_name }}
34+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
35+
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
36+
PUSH_BEFORE_SHA: ${{ github.event.before }}
37+
PUSH_HEAD_SHA: ${{ github.sha }}
38+
run: |
39+
range=
40+
if [ "$EVENT_NAME" = "pull_request" ]; then
41+
range="${PR_BASE_SHA}..${PR_HEAD_SHA}"
42+
elif [ -n "$PUSH_BEFORE_SHA" ] && [ "$PUSH_BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
43+
range="${PUSH_BEFORE_SHA}..${PUSH_HEAD_SHA}"
44+
fi
45+
46+
if [ -n "$range" ]; then
47+
scripts/check-commitlog.sh --range "$range"
48+
else
49+
scripts/check-commitlog.sh
50+
fi
51+
52+
# ---- Lint: formatting + static analysis (consolidated, one apt install) ----
53+
lint:
54+
runs-on: ubuntu-24.04
55+
steps:
56+
- name: Checkout
57+
uses: actions/checkout@v6
58+
59+
- name: Cache apt packages
60+
uses: actions/cache@v5
61+
with:
62+
path: ~/apt-cache
63+
key: apt-lint-${{ runner.os }}-${{ hashFiles('.github/workflows/main.yml') }}
64+
65+
- name: Install tools
66+
run: |
67+
mkdir -p ~/apt-cache
68+
sudo apt-get update
69+
sudo apt-get install -y -o Dir::Cache::Archives=$HOME/apt-cache \
70+
clang-format-20 cppcheck shfmt aspell
71+
72+
- name: Check trailing newline
73+
run: .ci/check-newline.sh
74+
75+
- name: Check clang-format
76+
run: .ci/check-format.sh
77+
78+
- name: Check banned functions
79+
run: .ci/check-banned.sh
80+
81+
- name: Static analysis (cppcheck)
82+
run: .ci/check-cppcheck.sh
83+
84+
- name: Check shell scripts (shfmt)
85+
run: .ci/check-shell.sh
86+
87+
# ---- Unit tests: no kernel dependency, fast ----
88+
unit-tests:
89+
runs-on: ubuntu-24.04
90+
steps:
91+
- name: Checkout
92+
uses: actions/checkout@v6
93+
94+
- name: Run unit tests
95+
run: make check-unit
96+
97+
# ---- Build kernel module + userspace compile check ----
98+
build:
99+
runs-on: ubuntu-24.04
100+
steps:
101+
- name: Checkout
102+
uses: actions/checkout@v6
103+
104+
- name: Install kernel headers
105+
run: |
106+
sudo apt-get update
107+
sudo apt-get install -y linux-headers-$(uname -r)
108+
109+
- name: Build kernel module and userspace
110+
run: make -j$(nproc)
111+
112+
# ---- Integration tests: builds and loads module on the same runner ----
113+
integration-tests:
114+
needs: build
115+
runs-on: ubuntu-24.04
116+
timeout-minutes: 10
117+
steps:
118+
- name: Checkout
119+
uses: actions/checkout@v6
120+
121+
- name: Install kernel headers
122+
run: |
123+
sudo apt-get update
124+
sudo apt-get install -y linux-headers-$(uname -r)
125+
126+
- name: Build kernel module and userspace
127+
run: make -j$(nproc)
128+
129+
- name: Integration tests
130+
run: sudo tests/test-integration.sh
131+
132+
- name: Kernel log diagnosis
133+
if: always()
134+
run: |
135+
echo "=== dmesg (last 100 lines) ==="
136+
sudo dmesg | tail -100
137+
echo ""
138+
echo "=== Module info ==="
139+
modinfo kxo.ko 2>/dev/null || echo "(module not found)"
140+
echo ""
141+
echo "=== Loaded modules (kxo) ==="
142+
lsmod | grep kxo || echo "(not loaded)"

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ USER_OBJS := user/xo-user.o user/tui.o user/coro.o
2626
VANITY_BASE := 0000e59602509f70319e2e4b915fcf1b9a1e2476
2727
VANITY_PREFIX := 0000
2828

29-
.PHONY: all kmod check-unit check cppcheck clean check-hashes
29+
.PHONY: all kmod check-unit check cppcheck clean check-hashes check-commitlog
3030

3131
ifneq ($(UNAME_S),Linux)
3232
define REQUIRE_LINUX
@@ -80,6 +80,9 @@ check: check-unit all
8080
cppcheck:
8181
@scripts/cppcheck.sh
8282

83+
check-commitlog:
84+
@scripts/check-commitlog.sh
85+
8386
$(GIT_HOOKS):
8487
@scripts/install-git-hooks
8588
@echo

include/game.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ typedef unsigned fixed_point_t;
7777

7878
extern const line_t lines[4];
7979

80+
/* Set by main.c when the module is shutting down. AI algorithms
81+
* should poll this in their hot loops to bail out early so that
82+
* flush_workqueue() in kxo_exit() does not block.
83+
*/
84+
extern bool kxo_stop_work;
85+
8086
int *available_moves(unsigned int table);
8187
char check_win(unsigned int t);
8288
fixed_point_t calculate_win_value(char win, unsigned char player);

include/mcts.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
#define ITERATIONS 100000
66

7+
/* Set by main.c when the module is shutting down. MCTS polls this in its
8+
* hot loop to bail out early so flush_workqueue() in kxo_exit() does not
9+
* block.
10+
*/
11+
extern bool kxo_stop_work;
12+
713
struct mcts_info {
814
struct state_array xoro_obj;
915
int nr_active_nodes;

0 commit comments

Comments
 (0)