Files
ledgrab/contexts/ci-cd.md
T
alexei.dolgolyov 6745e25b20 feat: roadmap batch (2026-06-19) — solar/linear-light/dither/nanoleaf + integrations
Eight roadmap features from the 2026-06-19 review, each a full vertical
(backend + tests + frontend + i18n en/ru/zh); ~67 new unit tests:

- automations: SolarRule sunrise/sunset trigger (new utils/solar.py, shared
  with the daylight cycle; window logic mirrors TimeOfDayRule)
- ci: best-effort arm64 multi-arch Docker manifest via QEMU + docker manifest
  (release.yml; amd64 path untouched, continue-on-error)
- game-integration: wire the orphaned LoLPoller via a LoLPollManager + a shared
  runtime_state module (poll lifecycle on enable/CRUD/startup/shutdown)
- ui: color-harmony gradient generator (complementary/analogous/triadic/...)
- effects: audio-reactive palette modulation (new audio_energy_tap; brightness/
  saturation modulation across all 12 procedural effects)
- capture: linear-light blending + spatio-temporal dithering, opt-in per
  calibration (new utils/linear_light.py, utils/dither.py)
- devices: Nanoleaf extControl v2 per-panel UDP streaming (per_panel mode)

Also bundles the pending 2026-06-18 production-review fixes and other
in-progress work already in the working tree (manual-trigger rule, etc.),
since they share files and could not be cleanly separated.

Gate: ruff + tsc clean; pytest 2654 passed / 2 skipped. The single failing
test (automation manual_trigger handler coverage) is a separate in-progress
item owned elsewhere, intentionally left as-is.
2026-06-22 23:21:24 +03:00

7.3 KiB

CI/CD & Release Workflow

Reference guide: Gitea Python CI/CD Guide — reusable patterns for Gitea Actions, cross-build, NSIS, Docker. When modifying workflows or build scripts, consult this guide to stay in sync with established patterns.

Workflows

File Trigger Purpose
.gitea/workflows/test.yml Push/PR to master Lint (ruff) + pytest
.gitea/workflows/release.yml Tag v* Build Windows/Linux/Docker artifacts + create Gitea release
.gitea/workflows/build-android.yml Push master (android/server paths), tag v*, manual Build Android APK; attach to release on tag

Release Pipeline (release.yml)

Four parallel jobs triggered by pushing a v* tag:

1. create-release

Creates the Gitea release with a description table listing all artifacts. The description must stay in sync with actual build outputs — if you add/remove/rename an artifact, update the body template here.

2. build-windows (cross-built from Linux)

  • Runs build/build-dist-windows.sh on Ubuntu with NSIS + msitools
  • Downloads Windows embedded Python 3.11 + pip wheels cross-platform
  • Bundles tkinter from Python MSI via msiextract
  • Builds frontend (npm run build)
  • Pre-compiles Python bytecode (compileall)
  • Produces: LedGrab-{tag}-win-x64.zip (portable) and LedGrab-{tag}-setup.exe (NSIS installer)

3. build-linux

  • Runs build/build-dist.sh on Ubuntu
  • Creates a venv, installs deps, builds frontend
  • Produces: LedGrab-{tag}-linux-x64.tar.gz

4. build-docker

  • Plain docker build + docker push for amd64 (no Buildx — TrueNAS runners lack nested networking)
  • Registry: {gitea_host}/{repo}:{tag}
  • Tags: v0.x.x, 0.x.x, and latest (stable only, not alpha/beta/rc)
  • arm64 is best-effort (Raspberry Pi / arm64 HAOS): a continue-on-error step cross-builds arm64 via QEMU binfmt (tonistiigi/binfmt) + docker manifest (NOT buildx — sidesteps the docker-container-driver networking limit) and folds amd64 + arm64 into multi-arch manifest lists under the same tags, plus :{tag}-amd64 / :{tag}-arm64 arch-suffixed tags. If the runner can't run privileged binfmt the step is skipped and the amd64 tags above remain valid.

Build Scripts

Script Platform Output
build/build-dist-windows.sh Linux → Windows cross-build ZIP + NSIS installer
build/build-dist.sh Linux native tarball
server/Dockerfile Docker Container image

Release Versioning

  • Tags: v{major}.{minor}.{patch} for stable, v{major}.{minor}.{patch}-alpha.{n} for pre-release
  • Pre-release tags set prerelease: true on the Gitea release
  • Docker latest tag only applied to stable releases
  • Version in server/pyproject.toml should match the tag (without v prefix)

CI Runners

  • Two TrueNAS Gitea runners with ubuntu tags
  • No Windows runner available — Windows builds are cross-compiled from Linux
  • Docker Buildx not available (networking limitations) — use plain docker build

Test Pipeline (test.yml)

  • Installs opencv-python-headless and libportaudio2 for CI compatibility
  • Display-dependent tests are skipped via @requires_display marker
  • Uses python not python3 (Git Bash on Windows resolves python3 to MS Store stub)

Version Detection Pattern

Build scripts use a fallback chain: CLI argument → exact git tag → CI env var (GITEA_REF_NAME / GITHUB_REF_NAME) → hardcoded in source. Always strip leading v for clean version strings.

NSIS Installer Best Practices

  • User-scoped install ($LOCALAPPDATA, RequestExecutionLevel user) — no admin required
  • Launch after install: Use MUI_FINISHPAGE_RUN_FUNCTION (not MUI_FINISHPAGE_RUN_PARAMETERS — NSIS Exec chokes on quoting). Still requires MUI_FINISHPAGE_RUN "" defined for checkbox visibility
  • Detect running instance: .onInit checks file lock on python.exe, offers to kill process before install
  • Uninstall preserves user data: Remove python/, app/, logs/ but NOT data/
  • CI build: sudo apt-get install -y nsis msitools zip then makensis -DVERSION="${VERSION}" build/installer.nsi

Hidden Launcher (VBS)

All shortcuts and the installer finish page use scripts/start-hidden.vbs instead of .bat to avoid console window flash. The VBS launcher must include an embedded Python fallback — installed distributions don't have Python on PATH, dev environment uses system Python.

Gitea vs GitHub Actions Differences

Feature GitHub Actions Gitea Actions
Context prefix github.* gitea.*
Ref name ${{ github.ref_name }} ${{ gitea.ref_name }}
Server URL ${{ github.server_url }} ${{ gitea.server_url }}
Output vars $GITHUB_OUTPUT $GITHUB_OUTPUT (same)
Secrets ${{ secrets.NAME }} ${{ secrets.NAME }} (same)
Docker Buildx Available May not work (runner networking)

Common Tasks

Creating a release

git tag v0.2.0
git push origin v0.2.0

Creating a pre-release

git tag v0.2.0-alpha.1
git push origin v0.2.0-alpha.1

Adding a new build artifact

  1. Update the build script to produce the new file
  2. Add upload step in the relevant build-* job
  3. Update the release description in create-release job body template
  4. Test with a pre-release tag first

Re-triggering a failed release workflow

# Option A: Delete and re-push the same tag
git push origin :refs/tags/v0.1.0-alpha.2
# Delete the release in Gitea UI or via API
git tag -f v0.1.0-alpha.2
git push origin v0.1.0-alpha.2

# Option B: Just bump the version (simpler)
git tag v0.1.0-alpha.3
git push origin v0.1.0-alpha.3

The create-release job has fallback logic — if the release already exists for a tag, it fetches and reuses the existing release ID.

Local Build Testing (Windows)

Prerequisites

  • NSIS: & "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe" install NSIS.NSIS
  • Installs to C:\Program Files (x86)\NSIS\makensis.exe

Build steps

npm ci && npm run build                                              # frontend
bash build/build-dist-windows.sh v1.0.0                              # Windows dist
"/c/Program Files (x86)/NSIS/makensis.exe" -DVERSION="1.0.0" build/installer.nsi  # installer

Iterating on installer only

If only installer.nsi changed (not app code), skip the full rebuild — just re-run makensis. If app code changed, re-run build/build-dist-windows.sh first since dist/ is a snapshot.

Common issues

Issue Fix
zip: command not found Git Bash doesn't include zip — harmless for installer builds
Exec expects 1 parameters, got 2 Use MUI_FINISHPAGE_RUN_FUNCTION instead of MUI_FINISHPAGE_RUN_PARAMETERS
Error opening file for writing: python\_asyncio.pyd Server is running — stop it before installing
App doesn't start after install VBS must use embedded Python fallback, not bare python
winget not recognized Use full path: $env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe
dist/ has stale files Re-run full build script — dist/ doesn't auto-update