diff --git a/deploy/repo-upgrade.sh b/deploy/repo-upgrade.sh index 45a83ff..4f71220 100644 --- a/deploy/repo-upgrade.sh +++ b/deploy/repo-upgrade.sh @@ -49,18 +49,23 @@ if ! git rev-parse --verify --quiet "refs/tags/$RELEASE_TAG" >/dev/null; then fi git checkout --detach "$RELEASE_TAG" test "$(git describe --tags --exact-match HEAD)" = "$RELEASE_TAG" -"$VENV/bin/pip" install --upgrade "$REPO_DIR" +"$VENV/bin/pip" install --upgrade --force-reinstall --no-cache-dir "$REPO_DIR" install -m 0644 "$REPO_DIR/deploy/importarr.service" /etc/systemd/system/importarr.service install -m 0755 "$REPO_DIR/deploy/repo-upgrade.sh" "$PREFIX/repo-upgrade.sh" systemctl daemon-reload GIT_SHA="$(git rev-parse --short=12 HEAD 2>/dev/null || printf development)" BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" VERSION="$RELEASE_TAG" -cat > "$PREFIX/build.env" < "$BUILD_ENV_TMP" </dev/null || printf development)" BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" VERSION="$(git -C "$REPO_DIR" describe --tags --exact-match HEAD 2>/dev/null || /opt/importarr/venv/bin/python -c 'from importarr import __version__; print(__version__)')" -cat > /opt/importarr/build.env < "$BUILD_ENV_TMP" < None: if not command: raise HTTPException(status_code=500, detail="update command is not configured") try: - subprocess.Popen( - ["/bin/sh", "-c", 'sleep 1; exec "$@"', "importarr-update", *command], - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, + result = subprocess.run( + ["systemd-run", "--unit=importarr-update", "--collect", "--no-block", "--on-active=2s", "--", *command], + check=False, + capture_output=True, + text=True, + timeout=10, ) + if result.returncode != 0: + raise HTTPException(status_code=500, detail=f"update command failed to schedule: {result.stderr[-1000:]}") + except subprocess.TimeoutExpired as exc: + raise HTTPException(status_code=504, detail="update command timed out while scheduling") from exc except OSError as exc: raise HTTPException(status_code=500, detail=f"update command failed to start: {exc.__class__.__name__}") from exc diff --git a/pyproject.toml b/pyproject.toml index 14a9caa..b1ab93c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "importarr" -version = "0.1.2" +version = "0.1.3" description = "Arr-style manual SABnzbd import service" readme = "README.md" requires-python = ">=3.12" @@ -23,6 +23,9 @@ test = ["pytest>=8.2", "pytest-asyncio>=0.23"] importarr = "importarr.main:run" manual-media-import = "importarr.worker:main" +[tool.hatch.build.targets.wheel] +packages = ["importarr"] + [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] diff --git a/tests/test_deploy_scripts.py b/tests/test_deploy_scripts.py index b677dcb..dd5ba8e 100644 --- a/tests/test_deploy_scripts.py +++ b/tests/test_deploy_scripts.py @@ -1,6 +1,8 @@ import fcntl import os import subprocess +import sys +import zipfile from pathlib import Path @@ -30,3 +32,28 @@ def test_repo_upgrade_refuses_concurrent_run(tmp_path): assert result.returncode == 1 assert "another Importarr upgrade is already running" in result.stderr + + +def test_wheel_contains_built_frontend_assets(tmp_path): + root = Path(__file__).parents[1] + subprocess.run( + [sys.executable, "-m", "pip", "wheel", "--no-deps", "--wheel-dir", str(tmp_path), str(root)], + check=True, + capture_output=True, + text=True, + ) + + wheel = next(tmp_path.glob("importarr-*.whl")) + with zipfile.ZipFile(wheel) as archive: + packaged = set(archive.namelist()) + + assert "importarr/static/index.html" in packaged + assert "importarr/static/assets/app.js" in packaged + assert "importarr/static/assets/app.css" in packaged + + +def test_repo_upgrade_forces_fresh_install_without_skipping_dependencies(): + script = (Path(__file__).parents[1] / "deploy/repo-upgrade.sh").read_text() + + assert "pip\" install --upgrade --force-reinstall --no-cache-dir" in script + assert "--no-deps" not in script diff --git a/tests/test_status.py b/tests/test_status.py index 00a792f..f353621 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -194,6 +194,32 @@ def test_update_schedules_exact_latest_release_tag(tmp_path, monkeypatch): assert response["status"] == "update_scheduled" +def test_update_is_scheduled_outside_service_cgroup(tmp_path, monkeypatch): + monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db")) + import importarr.main as main + + calls = [] + + def fake_run(command, **kwargs): + calls.append((command, kwargs)) + return main.subprocess.CompletedProcess(command, 0, stdout="Running as unit", stderr="") + + monkeypatch.setattr(main.subprocess, "run", fake_run) + main._schedule_update(["/bin/sh", "/opt/importarr/repo-upgrade.sh", "v1.2.3"]) + + assert calls[0][0] == [ + "systemd-run", + "--unit=importarr-update", + "--collect", + "--no-block", + "--on-active=2s", + "--", + "/bin/sh", + "/opt/importarr/repo-upgrade.sh", + "v1.2.3", + ] + + def test_update_endpoint_requires_expected_tag(tmp_path, monkeypatch): monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db")) import importarr.main as main