Files
plex-playlist/scripts/gitea-actions/deploy-runner-config.xsh
Cliff Hill d02039a22e
Some checks failed
CICD Start / Sanity and Base Decision (push) Successful in 18s
Runner Canary / Canary Heavy (ubuntu-act-8gb) (push) Has been skipped
Runner Canary / Canary Heavy (ubuntu-act-4gb) (push) Has been skipped
Runner Canary / Canary Burst (ubuntu-act (push) Failing after 11m10s
Runner Canary / Canary (ubuntu-latest) (push) Failing after 12m39s
Runner Canary / Canary (ubuntu-act) (push) Failing after 12m42s
Backend runtime upgraded to Python 3.14 with exact dependency pinning (#57)
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>

## Summary

Upgrades backend runtime baseline and dependency management for issue #10.

### Changes

1. **Python Baseline**: Updated from 3.13 to 3.14
   - Updated `backend/pyproject.toml` requires-python constraint
   - Updated `backend/pyrightconfig.json` pythonVersion
   - Updated all Dockerfile and CI references

2. **Dependency Pinning**: Switched to exact version pins in `backend/pyproject.toml`
   - All dev and runtime dependencies now use `==` instead of `>=`
   - `fastapi==0.120.2`, `uvicorn==0.38.0`
   - ruff, pyright, pytest suite pinned to current resolved versions
   - Regenerated `backend/uv.lock` under Python 3.14

3. **Startup Compatibility Guard** (TDD via RED→GREEN)
   - New `compatibility_status()` function evaluates runtime and pinned deps
   - Startup raises `RuntimeError` if policy fails
   - Implemented via FastAPI lifespan (non-deprecated) handler

4. **Compatibility Status Endpoint**
   - New `GET /compatibility` returns policy status, runtime version, and package checks
   - Shares single source of truth with startup validation

5. **Integration Tests**
   - Added failing-then-passing tests for startup guard and endpoint behavior
   - 100% coverage maintained

6. **Direnv Configuration**
   - Added `UV_PYTHON="3.14"` pin to repo `.envrc`
   - Ensures direnv creates/recreates venv with correct Python version

### Validation

-  ruff format/check
-  pyright strict (0 errors)
-  pytest: 8 passed, 100% coverage (>=95 gate)
-  pydoclint: pass
-  xdoctest: pass

### Notes

- SQLAlchemy/SQLModel introduction deferred to next pass per scope
- Compatibility logic currently validates fastapi/uvicorn pins (runtime deps)
- Ready for container build validation and Renovate bot testing

Co-authored-by: copilotcoder <copilotcoder@darkhelm.org>
Reviewed-on: #57
Co-authored-by: Cliff Hill <xlorep@darkhelm.org>
Co-committed-by: Cliff Hill <xlorep@darkhelm.org>
2026-06-18 11:19:24 -04:00

133 lines
4.2 KiB
Plaintext
Executable File

#!/usr/bin/env xonsh
"""Deploy shared runner config.yaml to all hosts and wire compose to use CONFIG_FILE."""
import os
import subprocess
import tempfile
from datetime import datetime
TEMPLATE_HOST = "kankali.darkhelm.lan"
TEMPLATE_PATH = "/home/darkhelm/Projects/DarkHelm.org/gitea/runner-config.template.yaml"
HOSTS = {
"kankali.darkhelm.lan": "/home/darkhelm/Projects/DarkHelm.org/gitea",
"zhokq.darkhelm.lan": "/home/darkhelm/Projects/DarkHelm.org/gitea-runner",
"urtzul.darkhelm.lan": "/home/darkhelm/Projects/DarkHelm.org/gitea-runner",
"pi-desktop.darkhelm.lan": "/home/darkhelm/Projects/DarkHelm.org/gitea-runner",
}
def ssh(host, cmd):
proc = subprocess.run(
[
"ssh",
"-o",
"ConnectTimeout=30",
"-o",
"ServerAliveInterval=30",
"-o",
"ServerAliveCountMax=6",
host,
cmd,
],
capture_output=True,
text=True,
)
return proc.returncode, proc.stdout + proc.stderr
def scp(local_path, host, remote_path):
proc = subprocess.run(
[
"scp",
"-o",
"ConnectTimeout=30",
"-o",
"ServerAliveInterval=30",
"-o",
"ServerAliveCountMax=6",
local_path,
f"{host}:{remote_path}",
],
capture_output=True,
text=True,
)
return proc.returncode, proc.stdout + proc.stderr
def build_compose(compose_text):
updated = compose_text
volume_line = " - ./daemon.json:/etc/docker/daemon.json:ro"
add_volume = " - ./config.yaml:/config.yaml:ro"
if add_volume not in updated and volume_line in updated:
updated = updated.replace(volume_line, volume_line + "\n" + add_volume)
force_pull_line = " - GITEA_RUNNER_JOB_CONTAINER_FORCE_PULL=${GITEA_RUNNER_JOB_CONTAINER_FORCE_PULL:-false}"
add_cfg_env = " - CONFIG_FILE=/config.yaml"
if add_cfg_env not in updated and force_pull_line in updated:
updated = updated.replace(force_pull_line, force_pull_line + "\n" + add_cfg_env)
return updated
print("Fetching canonical runner config template")
rc, template = ssh(TEMPLATE_HOST, f"cat {TEMPLATE_PATH}")
if rc != 0 or not template.strip():
print("Failed to fetch template from kankali")
raise SystemExit(2)
now = datetime.now().strftime("%Y%m%d_%H%M%S")
with tempfile.TemporaryDirectory() as tmpdir:
local_cfg = os.path.join(tmpdir, "config.yaml")
with open(local_cfg, "w", encoding="utf-8") as f:
f.write(template)
for host, root in HOSTS.items():
print(f"\n===== {host} =====")
rc, compose = ssh(host, f"cat {root}/compose.yml")
if rc != 0:
print("Could not read compose.yml")
continue
new_compose = build_compose(compose)
rc, _ = ssh(host, f"mkdir -p {root}/backups")
rc, _ = ssh(host, f"cp -f {root}/compose.yml {root}/backups/compose.yml.{now}")
rc, _ = ssh(host, f"test -f {root}/config.yaml")
if rc == 0:
ssh(host, f"cp -f {root}/config.yaml {root}/backups/config.yaml.{now}")
local_compose = os.path.join(tmpdir, f"compose.{host}.yml")
with open(local_compose, "w", encoding="utf-8") as f:
f.write(new_compose)
rc, out = scp(local_compose, host, f"{root}/compose.yml")
if rc != 0:
print("Failed to upload compose.yml")
print(out)
continue
rc, out = scp(local_cfg, host, f"{root}/config.yaml")
if rc != 0:
print("Failed to upload config.yaml")
print(out)
continue
rc, out = ssh(host, f"grep -n 'config.yaml:/config.yaml:ro' {root}/compose.yml")
print(out.strip())
rc, out = ssh(host, f"grep -n 'CONFIG_FILE=/config.yaml' {root}/compose.yml")
print(out.strip())
rc, out = ssh(host, f"docker compose -f {root}/compose.yml up -d act_runner_1")
print(out.strip())
rc, out = ssh(host, "docker inspect gitea-act-runner-1 --format '{{range .Mounts}}{{.Destination}} {{end}}'")
print("mounts:", out.strip())
print("Done")
print("\nDeployment complete")