# CI/CD & Release Workflow > **Reference guide:** [Gitea Python CI/CD Guide](https://git.dolgolyov-family.by/alexei.dolgolyov/claude-code-facts/src/branch/main/gitea-python-ci-cd.md) — 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 artifacts + create Gitea release | ## 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-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-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` (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) ## Build Scripts | Script | Platform | Output | |--------|----------|--------| | `build-dist-windows.sh` | Linux → Windows cross-build | ZIP + NSIS installer | | `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}" 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 ```bash git tag v0.2.0 git push origin v0.2.0 ``` ### Creating a pre-release ```bash 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 ```bash # 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 ```bash npm ci && npm run build # frontend bash build-dist-windows.sh v1.0.0 # Windows dist "/c/Program Files (x86)/NSIS/makensis.exe" -DVERSION="1.0.0" 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-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 |