Skip to content

Commit 2f652b8

Browse files
MTSistemiclaude
andcommitted
CI: build every app .deb from git + unit tests for the governor config logic
- scripts/build-debs-ci.sh assembles the 5 app packages purely from repo sources and then VERIFIES the packaged content (e.g. gov-mode present in the tuner helper) — the exact failure mode that produced a stale 26.06.5 build. - tests/: 9 pytest cases covering the multi-point GPU voltage curve, the Balanced/Performance mode switch, the gentle governor reload and the safe 2200 MHz fallback (the hard-freeze guards). - .github/workflows/ci.yml: syntax-check all apps, run tests, build debs, lintian (informational), upload artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f17dff1 commit 2f652b8

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI — apps build & verify
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'apps/**'
8+
- 'system/usr/**'
9+
- 'system/etc/**'
10+
- 'tests/**'
11+
- 'scripts/build-debs-ci.sh'
12+
- '.github/workflows/ci.yml'
13+
pull_request:
14+
branches: [main]
15+
workflow_dispatch:
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: actions/setup-python@v5
24+
with:
25+
python-version: '3.12'
26+
27+
- name: Syntax check every app script
28+
run: |
29+
set -e
30+
for f in apps/tuner/skillfish-tuner apps/tuner/skillfish-tuner-helper \
31+
apps/hub/skillfish-hub apps/hub/skillfish-hub-helper \
32+
apps/monitor/skillfish-monitor \
33+
apps/kernel-manager/skillfish-kernel-manager \
34+
apps/ai-panel/skillfish-ai-panel; do
35+
case "$(head -1 "$f")" in
36+
*python*) python -m py_compile "$f" && echo "compile OK: $f";;
37+
*) bash -n "$f" && echo "bash -n OK: $f";;
38+
esac
39+
done
40+
41+
- name: Unit tests (governor config logic)
42+
run: |
43+
pip install -q pytest
44+
python -m pytest tests/ -v
45+
46+
- name: Build all .debs from git sources
47+
run: |
48+
chmod +x scripts/build-debs-ci.sh
49+
./scripts/build-debs-ci.sh "0.0~ci${{ github.run_number }}"
50+
51+
- name: Lintian (informational)
52+
run: |
53+
sudo apt-get -qq install -y lintian >/dev/null
54+
lintian --no-tag-display-limit /tmp/sfx-debs/out/*.deb || true
55+
56+
- name: Upload .deb artifacts
57+
uses: actions/upload-artifact@v4
58+
with:
59+
name: skillfish-debs
60+
path: /tmp/sfx-debs/out/*.deb
61+
retention-days: 14

scripts/build-debs-ci.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/bin/bash
2+
# Build the SkillFishOS app .debs from the repo sources (CI / clean machine).
3+
# Unlike the on-box builders, every input comes from git — this catches the
4+
# "stale binary packaged into a new version" failure mode before publishing.
5+
set -euo pipefail
6+
cd "$(dirname "$0")/.."
7+
VER="${1:-0.0~ci$(date +%Y%m%d)}"
8+
OUT="${OUT:-/tmp/sfx-debs}"
9+
rm -rf "$OUT"; mkdir -p "$OUT/out"
10+
11+
put() { # put <pkg> <mode> <src> <dest-rel>
12+
[ -f "$3" ] || { echo "FATAL: missing source file $3" >&2; exit 1; }
13+
install -D -m "$2" "$3" "$OUT/$1/$4"
14+
}
15+
opt() { # like put, but optional
16+
[ -f "$3" ] && install -D -m "$2" "$3" "$OUT/$1/$4" || echo " (optional, skipped: $3)"
17+
}
18+
ctrl() { # ctrl <pkg> <depends> <desc-first-line>
19+
mkdir -p "$OUT/$1/DEBIAN"
20+
printf 'Package: %s\nVersion: %s\nArchitecture: all\nMaintainer: SkillFishOS <info@skillfishos.com>\nDepends: %s\nSection: utils\nPriority: optional\nHomepage: https://skillfishos.com\nDescription: %s\n built from git by CI.\n' \
21+
"$1" "$VER" "$2" "$3" > "$OUT/$1/DEBIAN/control"
22+
printf '#!/bin/sh\nset -e\nupdate-desktop-database -q 2>/dev/null || true\ngtk-update-icon-cache -q -f /usr/share/icons/hicolor 2>/dev/null || true\nappstreamcli refresh-cache --force >/dev/null 2>&1 || true\nexit 0\n' > "$OUT/$1/DEBIAN/postinst"
23+
chmod 0755 "$OUT/$1/DEBIAN/postinst"
24+
}
25+
shot() { # shot <pkg> <metainfo-path>: install metainfo + its referenced screenshots
26+
put "$1" 0644 "$2" "usr/share/metainfo/$(basename "$2")"
27+
for s in $(grep -oE 'screenshots/[A-Za-z0-9._-]+' "$2" | sed 's#screenshots/##' | sort -u); do
28+
opt "$1" 0644 "screenshots/$s" "usr/share/skillfish/screenshots/$s"
29+
done
30+
}
31+
32+
P=skillfish-tuner
33+
put $P 0755 apps/tuner/skillfish-tuner usr/local/bin/skillfish-tuner
34+
put $P 0755 apps/tuner/skillfish-tuner-helper usr/local/bin/skillfish-tuner-helper
35+
put $P 0755 system/usr/local/bin/skillfish-cu usr/local/bin/skillfish-cu
36+
put $P 0755 system/usr/local/bin/skillfish-hud-val usr/local/bin/skillfish-hud-val
37+
put $P 0755 system/usr/local/bin/skillfish-hud-bt usr/local/bin/skillfish-hud-bt
38+
put $P 0644 system/usr/share/skillfish/tuner-presets.json usr/share/skillfish/tuner-presets.json
39+
put $P 0644 system/usr/share/applications/os.skillfish.Tuner.desktop usr/share/applications/os.skillfish.Tuner.desktop
40+
put $P 0644 system/usr/share/icons/hicolor/256x256/apps/skillfish-tuner.png usr/share/icons/hicolor/256x256/apps/skillfish-tuner.png
41+
put $P 0644 system/etc/systemd/system/skillfish-cu.service etc/systemd/system/skillfish-cu.service
42+
opt $P 0644 system/usr/share/polkit-1/actions/os.skillfish.tuner.policy usr/share/polkit-1/actions/os.skillfish.tuner.policy
43+
shot $P apps/tuner/os.skillfish.Tuner.metainfo.xml
44+
ctrl $P "python3, python3-pyqt6, polkitd | policykit-1" "SkillFishOS Tuner - BC-250 hardware control GUI"
45+
46+
P=skillfish-hub
47+
put $P 0755 apps/hub/skillfish-hub usr/local/bin/skillfish-hub
48+
put $P 0755 apps/hub/skillfish-hub-helper usr/local/bin/skillfish-hub-helper
49+
put $P 0644 system/usr/share/applications/os.skillfish.hub.desktop usr/share/applications/os.skillfish.hub.desktop
50+
shot $P apps/hub/os.skillfish.hub.metainfo.xml
51+
ctrl $P "python3, python3-pyqt6, python3-apt, gir1.2-appstream-1.0, appstream, curl, polkitd | policykit-1" "SkillFishOS Hub - Discover-style software centre"
52+
53+
P=skillfish-monitor
54+
put $P 0755 apps/monitor/skillfish-monitor usr/local/bin/skillfish-monitor
55+
put $P 0644 system/usr/share/applications/os.skillfish.monitor.desktop usr/share/applications/os.skillfish.monitor.desktop
56+
shot $P apps/monitor/os.skillfish.monitor.metainfo.xml
57+
ctrl $P "python3, python3-pyqt6" "SkillFishOS Monitor - live sensor charts"
58+
59+
P=skillfish-kernel-manager
60+
put $P 0755 apps/kernel-manager/skillfish-kernel-manager usr/local/bin/skillfish-kernel-manager
61+
put $P 0755 apps/kernel-manager/skillfish-kernel-helper usr/local/bin/skillfish-kernel-helper
62+
put $P 0644 system/usr/share/applications/os.skillfish.kernel.desktop usr/share/applications/os.skillfish.kernel.desktop
63+
shot $P apps/kernel-manager/os.skillfish.kernel.metainfo.xml
64+
ctrl $P "python3, python3-pyqt6, polkitd | policykit-1" "SkillFishOS Kernel Manager"
65+
66+
P=skillfish-ai-panel
67+
put $P 0755 apps/ai-panel/skillfish-ai-panel usr/local/bin/skillfish-ai-panel
68+
put $P 0755 apps/ai-panel/skillfish-gtt usr/local/bin/skillfish-gtt
69+
put $P 0644 system/usr/share/applications/os.skillfish.ai.desktop usr/share/applications/os.skillfish.ai.desktop
70+
shot $P apps/ai-panel/os.skillfish.ai.metainfo.xml
71+
ctrl $P "python3, python3-pyqt6, polkitd | policykit-1" "SkillFish AI - on-device LLM control panel"
72+
73+
echo "== building =="
74+
for P in skillfish-tuner skillfish-hub skillfish-monitor skillfish-kernel-manager skillfish-ai-panel; do
75+
find "$OUT/$P" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
76+
dpkg-deb --root-owner-group --build "$OUT/$P" "$OUT/out/${P}_${VER}_all.deb" >/dev/null
77+
done
78+
ls -l "$OUT/out"
79+
80+
echo "== content verification (the bogus-deb guard) =="
81+
check() { dpkg-deb --fsys-tarfile "$OUT/out/$1" | tar -xO "$2" | grep -q "$3" \
82+
&& echo "OK $1: $2 contains '$3'" || { echo "FAIL $1: $2 missing '$3'" >&2; exit 1; }; }
83+
check skillfish-tuner_${VER}_all.deb ./usr/local/bin/skillfish-tuner-helper gov-mode
84+
check skillfish-tuner_${VER}_all.deb ./usr/local/bin/skillfish-tuner gov_perf
85+
check skillfish-hub_${VER}_all.deb ./usr/local/bin/skillfish-hub "return None"
86+
check skillfish-kernel-manager_${VER}_all.deb ./usr/local/bin/skillfish-kernel-manager skillfish
87+
check skillfish-ai-panel_${VER}_all.deb ./usr/local/bin/skillfish-ai-panel skillfish
88+
echo "ALL DEBS VERIFIED"

tests/test_tuner_helper.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Tests for the pure logic of skillfish-tuner-helper (no root, no SMU, no Qt).
2+
3+
These guard the GPU-governor config writer: the multi-point voltage curve and
4+
the Balanced/Performance mode switch. A wrong curve here can hard-freeze a
5+
BC-250 (see docs/OPTIMIZATIONS.md), so this is the most safety-critical pure
6+
logic in the repo.
7+
"""
8+
import importlib.machinery
9+
import importlib.util
10+
import pathlib
11+
import types
12+
13+
import pytest
14+
15+
HELPER = pathlib.Path(__file__).resolve().parents[1] / "apps" / "tuner" / "skillfish-tuner-helper"
16+
17+
18+
@pytest.fixture()
19+
def helper(tmp_path, monkeypatch):
20+
loader = importlib.machinery.SourceFileLoader("tuner_helper", str(HELPER))
21+
spec = importlib.util.spec_from_loader("tuner_helper", loader)
22+
mod = importlib.util.module_from_spec(spec)
23+
loader.exec_module(mod)
24+
# never touch the real system from tests
25+
conf = tmp_path / "config.toml"
26+
monkeypatch.setattr(mod, "GOV_CONF", str(conf))
27+
calls = []
28+
monkeypatch.setattr(mod, "sh", lambda cmd, timeout=30: (calls.append(cmd), types.SimpleNamespace(returncode=0, stdout=""))[1])
29+
mod._test_calls = calls
30+
mod._test_conf = conf
31+
return mod
32+
33+
34+
# ---------- _gpu_curve: the multi-point voltage ladder ----------
35+
36+
def test_curve_full_range_inserts_midpoints(helper):
37+
assert helper._gpu_curve(350, 700, 2200, 1000) == [(350, 700), (1500, 900), (2000, 1000), (2200, 1000)]
38+
39+
40+
def test_curve_low_max_has_no_midpoints_above_it(helper):
41+
assert helper._gpu_curve(350, 700, 1500, 900) == [(350, 700), (1500, 900)]
42+
43+
44+
def test_curve_dedup_when_max_is_a_midpoint(helper):
45+
pts = helper._gpu_curve(350, 700, 2000, 1000)
46+
assert pts == [(350, 700), (1500, 900), (2000, 1000)]
47+
freqs = [f for f, _ in pts]
48+
assert len(freqs) == len(set(freqs)), "no duplicate frequencies"
49+
50+
51+
def test_curve_is_ascending(helper):
52+
pts = helper._gpu_curve(350, 700, 2200, 1000)
53+
assert pts == sorted(pts)
54+
55+
56+
# ---------- apply_gpu: writes the curve, replaces old safe-points ----------
57+
58+
def test_apply_gpu_writes_multipoint_curve(helper):
59+
helper._test_conf.write_text("[load-target]\nupper = 0.95\nlower = 0.7\n"
60+
"[[safe-points]]\nfrequency = 350\nvoltage = 700\n"
61+
"[[safe-points]]\nfrequency = 2230\nvoltage = 1000\n")
62+
assert helper.apply_gpu(350, 700, 2200, 1000)
63+
txt = helper._test_conf.read_text()
64+
assert "2230" not in txt, "the dangerous 2230 point must be gone"
65+
assert txt.count("[[safe-points]]") == 4
66+
assert "frequency = 1500" in txt and "frequency = 2000" in txt
67+
68+
69+
def test_apply_gpu_strips_commented_safepoints(helper):
70+
helper._test_conf.write_text("[load-target]\nupper = 0.95\nlower = 0.7\n"
71+
"[[safe-points]]\nfrequency = 350 # MHz\nvoltage = 700 # mV\n")
72+
assert helper.apply_gpu(350, 700, 2200, 1000)
73+
txt = helper._test_conf.read_text()
74+
assert txt.count("[[safe-points]]") == 4
75+
76+
77+
def test_apply_gpu_reloads_governor_gently(helper):
78+
helper._test_conf.write_text("[[safe-points]]\nfrequency = 350\nvoltage = 700\n"
79+
"[[safe-points]]\nfrequency = 2000\nvoltage = 1000\n")
80+
helper.apply_gpu(350, 700, 2000, 1000)
81+
joined = " | ".join(helper._test_calls)
82+
assert "stop cyan-skillfish-governor" in joined and "start cyan-skillfish-governor" in joined
83+
assert "restart" not in joined, "must use stop -> settle -> start, not an abrupt restart"
84+
85+
86+
# ---------- gov_mode: Balanced / Performance switch ----------
87+
88+
def _stock(helper):
89+
helper._test_conf.write_text("[timing.intervals]\nsample = 2000\nadjust = 20_000\nfinetune = 1_000_000_000\n"
90+
"[timing.ramp-rates]\nnormal = 1\nburst = 200\n[timing]\nburst-samples = 48\n"
91+
"[frequency-thresholds]\nadjust = 100\nfinetune = 10\n"
92+
"[load-target]\nupper = 0.95\nlower = 0.7\n"
93+
"[[safe-points]]\nfrequency = 350\nvoltage = 700\n"
94+
"[[safe-points]]\nfrequency = 1500\nvoltage = 900\n"
95+
"[[safe-points]]\nfrequency = 2000\nvoltage = 1000\n"
96+
"[[safe-points]]\nfrequency = 2200\nvoltage = 1000\n")
97+
98+
99+
def test_mode_roundtrip_preserves_safepoints(helper):
100+
_stock(helper)
101+
assert helper.current_gov_mode() == "balanced"
102+
assert helper.gov_mode("performance")
103+
txt = helper._test_conf.read_text()
104+
assert "upper = 0.20" in txt and helper.current_gov_mode() == "performance"
105+
assert txt.count("[[safe-points]]") == 4, "performance keeps the user's curve"
106+
assert helper.gov_mode("balanced")
107+
txt = helper._test_conf.read_text()
108+
assert "upper = 0.95" in txt and helper.current_gov_mode() == "balanced"
109+
assert txt.count("[[safe-points]]") == 4
110+
111+
112+
def test_safepoints_fallback_is_the_safe_curve(helper):
113+
helper._test_conf.write_text("") # unreadable / empty config
114+
pts = helper._gov_safepoints()
115+
assert pts == [(350, 700), (1500, 900), (2000, 1000), (2200, 1000)]
116+
assert max(f for f, _ in pts) <= 2200, "fallback must never exceed the validated 2200 MHz"

0 commit comments

Comments
 (0)