docs: add Gitea Python CI/CD guide
Reusable reference extracted from wled-screen-controller covering: - Lint/test and release workflows for Gitea Actions - Cross-building Windows from Linux (embedded Python + wheels) - NSIS installer packaging - Docker multi-stage builds with Gitea registry - Version detection, pre-release handling, size optimization
This commit is contained in:
@@ -35,3 +35,7 @@ Fast code search skill for Claude Code — find classes, symbols, usages, implem
|
|||||||
### [Everything Claude Code (ECC) — Setup Guide](ecc-setup-guide.md)
|
### [Everything Claude Code (ECC) — Setup Guide](ecc-setup-guide.md)
|
||||||
|
|
||||||
Step-by-step instructions for setting up [Everything Claude Code](https://github.com/affaan-m/everything-claude-code) on a new machine — a comprehensive collection of skills, rules, agents, and hooks for Claude Code.
|
Step-by-step instructions for setting up [Everything Claude Code](https://github.com/affaan-m/everything-claude-code) on a new machine — a comprehensive collection of skills, rules, agents, and hooks for Claude Code.
|
||||||
|
|
||||||
|
### [CI/CD for Python Apps on Gitea](gitea-python-ci-cd.md)
|
||||||
|
|
||||||
|
Reusable reference for building CI pipelines, release automation, and installer packaging for Python apps on Gitea — covering lint/test workflows, cross-compiled Windows builds (embedded Python + NSIS), Linux tarballs, Docker images, and Gitea REST API release management.
|
||||||
|
|||||||
455
gitea-python-ci-cd.md
Normal file
455
gitea-python-ci-cd.md
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
# CI/CD for Python Apps on Gitea
|
||||||
|
|
||||||
|
A reusable reference for building CI pipelines, release automation, and installer packaging for Python-based applications hosted on Gitea. Extracted from a production project using Gitea Actions (GitHub Actions-compatible).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Gitea instance with Actions enabled
|
||||||
|
- Runner(s) tagged `ubuntu-latest` (e.g., TrueNAS-hosted Gitea runners)
|
||||||
|
- `GITEA_TOKEN` secret configured in the repository (Settings > Secrets)
|
||||||
|
|
||||||
|
## Pipeline Architecture
|
||||||
|
|
||||||
|
Two workflows, triggered by different events:
|
||||||
|
|
||||||
|
```
|
||||||
|
push/PR to master ──► test.yml (lint + test)
|
||||||
|
push tag v* ──► release.yml (build + release + Docker)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Lint & Test Workflow
|
||||||
|
|
||||||
|
**Trigger:** Push to `master` or pull request.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/test.yml
|
||||||
|
name: Lint & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends <your-system-deps>
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: server
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
working-directory: server
|
||||||
|
run: ruff check src/ tests/
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
working-directory: server
|
||||||
|
run: pytest --tb=short -q
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points:**
|
||||||
|
|
||||||
|
- Keep the test job fast — lint first, then test
|
||||||
|
- Use `ruff` for linting (fast, replaces flake8 + isort + pyupgrade)
|
||||||
|
- Configure pytest in `pyproject.toml` with coverage:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
addopts = "-v --cov=your_package --cov-report=html --cov-report=term"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Release Workflow
|
||||||
|
|
||||||
|
**Trigger:** Push a tag matching `v*` (e.g., `v0.2.0`, `v0.2.0-alpha.1`).
|
||||||
|
|
||||||
|
### 2.1. Create Release Job
|
||||||
|
|
||||||
|
Creates a Gitea release via REST API with a formatted description.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_id: ${{ steps.create.outputs.release_id }}
|
||||||
|
steps:
|
||||||
|
- name: Create Gitea release
|
||||||
|
id: create
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
|
||||||
|
# Detect pre-release
|
||||||
|
IS_PRE="false"
|
||||||
|
if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
|
||||||
|
IS_PRE="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build release body (use Python to avoid YAML escaping issues)
|
||||||
|
BODY_JSON=$(python3 -c "
|
||||||
|
import json, textwrap
|
||||||
|
tag = '$TAG'
|
||||||
|
body = '''## Downloads
|
||||||
|
| Platform | File |
|
||||||
|
|----------|------|
|
||||||
|
| Windows (installer) | \`App-{tag}-setup.exe\` |
|
||||||
|
| Windows (portable) | \`App-{tag}-win-x64.zip\` |
|
||||||
|
| Linux | \`App-{tag}-linux-x64.tar.gz\` |
|
||||||
|
'''
|
||||||
|
print(json.dumps(textwrap.dedent(body).strip()))
|
||||||
|
")
|
||||||
|
|
||||||
|
RELEASE=$(curl -s -X POST "\$BASE_URL/releases" \
|
||||||
|
-H "Authorization: token \$GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"\$TAG\",
|
||||||
|
\"name\": \"App \$TAG\",
|
||||||
|
\"body\": \$BODY_JSON,
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": \$IS_PRE
|
||||||
|
}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "\$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||||
|
echo "release_id=\$RELEASE_ID" >> "\$GITHUB_OUTPUT"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points:**
|
||||||
|
|
||||||
|
- Gitea uses `${{ gitea.ref_name }}` instead of GitHub's `${{ github.ref_name }}`
|
||||||
|
- Use `${{ gitea.server_url }}` and `${{ gitea.repository }}` for API URLs
|
||||||
|
- Output vars use `$GITHUB_OUTPUT` (Gitea Actions is compatible with GitHub Actions syntax)
|
||||||
|
- Use Python for body templating to avoid shell/YAML escaping hell
|
||||||
|
|
||||||
|
### 2.2. Build Jobs (Parallel)
|
||||||
|
|
||||||
|
All build jobs run in parallel, gated on `create-release`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
build-windows:
|
||||||
|
needs: create-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# ...
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
needs: create-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# ...
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
needs: create-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3. Upload Assets to Gitea Release
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Attach assets to release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||||
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
|
||||||
|
FILE=$(ls build/App-*.zip | head -1)
|
||||||
|
curl -s -X POST \
|
||||||
|
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$FILE")" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$FILE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Version Detection Pattern
|
||||||
|
|
||||||
|
A robust fallback chain for detecting the version in build scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. CLI argument
|
||||||
|
VERSION="${1:-}"
|
||||||
|
|
||||||
|
# 2. Exact git tag
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=$(git describe --tags --exact-match 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. CI environment variable (Gitea or GitHub)
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Hardcoded in source
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' \
|
||||||
|
src/your_package/__init__.py 2>/dev/null || echo "0.0.0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_CLEAN="${VERSION#v}" # Strip leading 'v'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Cross-Building Windows from Linux
|
||||||
|
|
||||||
|
No Windows runner needed. Download Windows embedded Python and win_amd64 wheels on Linux.
|
||||||
|
|
||||||
|
### 4.1. Embedded Python Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download Windows embedded Python
|
||||||
|
PYTHON_VERSION="3.11.9"
|
||||||
|
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
|
||||||
|
-o python-embed.zip
|
||||||
|
unzip -qo python-embed.zip -d dist/python
|
||||||
|
|
||||||
|
# Patch ._pth to enable site-packages
|
||||||
|
PTH_FILE=$(ls dist/python/python*._pth | head -1)
|
||||||
|
sed -i 's/^#\s*import site/import site/' "$PTH_FILE"
|
||||||
|
echo 'Lib\site-packages' >> "$PTH_FILE"
|
||||||
|
echo '../app/src' >> "$PTH_FILE" # App source on PYTHONPATH
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why embedded Python?** It's a minimal, standalone Python distribution (~15 MB) without an installer. Perfect for bundling into portable apps.
|
||||||
|
|
||||||
|
### 4.2. Cross-Platform Wheel Download
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WHEEL_DIR="build/win-wheels"
|
||||||
|
SITE_PACKAGES="dist/python/Lib/site-packages"
|
||||||
|
mkdir -p "$WHEEL_DIR" "$SITE_PACKAGES"
|
||||||
|
|
||||||
|
# Download win_amd64 wheels (fallback to source for pure Python)
|
||||||
|
for dep in "fastapi>=0.115.0" "uvicorn>=0.32.0" "numpy>=2.1.3"; do
|
||||||
|
pip download --quiet --dest "$WHEEL_DIR" \
|
||||||
|
--platform win_amd64 --python-version "3.11" \
|
||||||
|
--implementation cp --only-binary :all: \
|
||||||
|
"$dep" 2>/dev/null || \
|
||||||
|
pip download --quiet --dest "$WHEEL_DIR" "$dep"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Install wheels into site-packages (just unzip — .whl is a zip)
|
||||||
|
for whl in "$WHEEL_DIR"/*.whl; do
|
||||||
|
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. Size Optimization
|
||||||
|
|
||||||
|
Strip unnecessary files from site-packages to reduce archive size:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find "$SITE_PACKAGES" -type d -name __pycache__ -exec rm -rf {} +
|
||||||
|
find "$SITE_PACKAGES" -type d -name tests -exec rm -rf {} +
|
||||||
|
find "$SITE_PACKAGES" -type d -name "*.dist-info" -exec rm -rf {} +
|
||||||
|
find "$SITE_PACKAGES" -name "*.pyi" -delete
|
||||||
|
|
||||||
|
# Remove build-time-only packages
|
||||||
|
rm -rf "$SITE_PACKAGES"/{pip,setuptools,pkg_resources}*
|
||||||
|
|
||||||
|
# Remove heavy unused parts of specific libraries
|
||||||
|
rm -f "$SITE_PACKAGES"/cv2/opencv_videoio_ffmpeg*.dll # -28 MB
|
||||||
|
rm -rf "$SITE_PACKAGES"/numpy/{tests,f2py,typing}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Bundling tkinter (Optional)
|
||||||
|
|
||||||
|
Embedded Python doesn't include tkinter. Extract it from the official MSI packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires: apt install msitools
|
||||||
|
MSI_BASE="https://www.python.org/ftp/python/${PYTHON_VERSION}/amd64"
|
||||||
|
curl -sL "$MSI_BASE/tcltk.msi" -o tcltk.msi
|
||||||
|
curl -sL "$MSI_BASE/lib.msi" -o lib.msi
|
||||||
|
|
||||||
|
msiextract tcltk.msi # → _tkinter.pyd, tcl86t.dll, tk86t.dll, tcl8.6/, tk8.6/
|
||||||
|
msiextract lib.msi # → tkinter/ Python package
|
||||||
|
|
||||||
|
# Copy to embedded Python directory
|
||||||
|
cp _tkinter.pyd tcl86t.dll tk86t.dll dist/python/
|
||||||
|
cp -r tkinter dist/python/Lib/
|
||||||
|
cp -r tcl8.6 tk8.6 dist/python/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Linux Build (venv-based)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create self-contained virtualenv
|
||||||
|
python3 -m venv dist/venv
|
||||||
|
source dist/venv/bin/activate
|
||||||
|
pip install ".[camera,notifications]"
|
||||||
|
|
||||||
|
# Remove the installed package (PYTHONPATH handles app code at runtime)
|
||||||
|
rm -rf dist/venv/lib/python*/site-packages/your_package*
|
||||||
|
|
||||||
|
# Launcher script
|
||||||
|
cat > dist/run.sh << 'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
export PYTHONPATH="$SCRIPT_DIR/app/src"
|
||||||
|
source "$SCRIPT_DIR/venv/bin/activate"
|
||||||
|
exec python -m your_package.main
|
||||||
|
EOF
|
||||||
|
chmod +x dist/run.sh
|
||||||
|
|
||||||
|
# Package
|
||||||
|
tar -czf "App-v${VERSION}-linux-x64.tar.gz" dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.1. Systemd Service Script
|
||||||
|
|
||||||
|
Include install/uninstall scripts for running as a service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install-service.sh creates:
|
||||||
|
# /etc/systemd/system/your-app.service
|
||||||
|
# Type=simple, Restart=on-failure, RestartSec=5
|
||||||
|
# ExecStart=/path/to/run.sh
|
||||||
|
# Then: systemctl daemon-reload && systemctl enable && systemctl start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. NSIS Installer (Windows)
|
||||||
|
|
||||||
|
Cross-compilable on Linux: `apt install nsis && makensis installer.nsi`
|
||||||
|
|
||||||
|
Key design decisions:
|
||||||
|
|
||||||
|
```nsi
|
||||||
|
; User-scoped install (no admin required)
|
||||||
|
InstallDir "$LOCALAPPDATA\${APPNAME}"
|
||||||
|
RequestExecutionLevel user
|
||||||
|
|
||||||
|
; Sections
|
||||||
|
Section "!Core (required)" SecCore ; App files + uninstaller
|
||||||
|
Section "Desktop shortcut" SecDesktop ; Optional
|
||||||
|
Section "Start with Windows" SecAutostart ; Optional — Startup folder shortcut
|
||||||
|
|
||||||
|
; Uninstall preserves user data
|
||||||
|
; RMDir /r $INSTDIR\python, $INSTDIR\app, $INSTDIR\logs
|
||||||
|
; But NOT $INSTDIR\data (user config)
|
||||||
|
```
|
||||||
|
|
||||||
|
**CI dependencies:** `sudo apt-get install -y nsis msitools zip`
|
||||||
|
|
||||||
|
Build: `makensis -DVERSION="${VERSION}" installer.nsi`
|
||||||
|
|
||||||
|
## 7. Docker Build
|
||||||
|
|
||||||
|
### Multi-stage Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Build frontend
|
||||||
|
FROM node:20-slim AS frontend
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Runtime
|
||||||
|
FROM python:3.11-slim
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends <system-deps> \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install --no-cache-dir ".[notifications]"
|
||||||
|
COPY src/ src/
|
||||||
|
COPY --from=frontend /app/src/your_package/static/ src/your_package/static/
|
||||||
|
|
||||||
|
RUN useradd -r -u 1000 appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["uvicorn", "your_package.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker in CI (Gitea Registry)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
id: docker-login
|
||||||
|
continue-on-error: true # Graceful degradation if registry unavailable
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GITEA_TOKEN }}" | docker login \
|
||||||
|
"$SERVER_HOST" -u "${{ gitea.actor }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build and tag
|
||||||
|
if: steps.docker-login.outcome == 'success'
|
||||||
|
run: |
|
||||||
|
docker build -t "$REGISTRY:$TAG" -t "$REGISTRY:$VERSION" ./server
|
||||||
|
# Tag as 'latest' only for stable releases
|
||||||
|
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
|
||||||
|
docker tag "$REGISTRY:$TAG" "$REGISTRY:latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Push
|
||||||
|
if: steps.docker-login.outcome == 'success'
|
||||||
|
run: docker push "$REGISTRY" --all-tags
|
||||||
|
```
|
||||||
|
|
||||||
|
**Registry URL pattern:** `{gitea-host}/{owner}/{repo}:{tag}`
|
||||||
|
|
||||||
|
## 8. Release Versioning
|
||||||
|
|
||||||
|
| Tag format | Example | Pre-release? | Docker `:latest`? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `v{major}.{minor}.{patch}` | `v0.2.0` | No | Yes |
|
||||||
|
| `v{major}.{minor}.{patch}-alpha.{n}` | `v0.2.0-alpha.1` | Yes | No |
|
||||||
|
| `v{major}.{minor}.{patch}-beta.{n}` | `v0.2.0-beta.2` | Yes | No |
|
||||||
|
| `v{major}.{minor}.{patch}-rc.{n}` | `v0.2.0-rc.1` | Yes | No |
|
||||||
|
|
||||||
|
**Release process:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stable release
|
||||||
|
git tag v0.2.0
|
||||||
|
git push origin v0.2.0
|
||||||
|
|
||||||
|
# Pre-release
|
||||||
|
git tag v0.2.0-alpha.1
|
||||||
|
git push origin v0.2.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 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 }}` |
|
||||||
|
| Repository | `${{ github.repository }}` | `${{ gitea.repository }}` |
|
||||||
|
| Actor | `${{ github.actor }}` | `${{ gitea.actor }}` |
|
||||||
|
| SHA | `${{ github.sha }}` | `${{ gitea.sha }}` |
|
||||||
|
| Output vars | `$GITHUB_OUTPUT` | `$GITHUB_OUTPUT` (same) |
|
||||||
|
| Secrets | `${{ secrets.NAME }}` | `${{ secrets.NAME }}` (same) |
|
||||||
|
| Docker Buildx | Available | May not work (runner networking) |
|
||||||
|
|
||||||
|
## 10. Checklist for New Projects
|
||||||
|
|
||||||
|
- [ ] Create `.gitea/workflows/test.yml` — lint + test on push/PR
|
||||||
|
- [ ] Create `.gitea/workflows/release.yml` — build + release on `v*` tag
|
||||||
|
- [ ] Add `GITEA_TOKEN` secret to repository
|
||||||
|
- [ ] Set up version detection in build scripts (tag → env → source)
|
||||||
|
- [ ] Create `build-dist.sh` for Linux (venv + tarball)
|
||||||
|
- [ ] Create `build-dist-windows.sh` for Windows (embedded Python + ZIP)
|
||||||
|
- [ ] Create `installer.nsi` for Windows installer (optional)
|
||||||
|
- [ ] Create `Dockerfile` for Docker builds (optional)
|
||||||
|
- [ ] Configure `pyproject.toml` with `[tool.ruff]` and `[tool.pytest]`
|
||||||
|
- [ ] Set up `.pre-commit-config.yaml` with ruff + black
|
||||||
Reference in New Issue
Block a user