From fb955adaf878f349ccc3775b07d22cf655479667 Mon Sep 17 00:00:00 2001 From: Daniel Gradman-Svendsen Date: Thu, 30 Jul 2026 14:36:45 +0200 Subject: [PATCH] Add persistent local review stack --- .dockerignore | 14 ++++ .gitignore | 2 + README.md | 36 ++++++++++ deploy/docker-compose.review.yml | 22 ++++++ deploy/importarr.review.env.example | 23 ++++++ deploy/review/review.sh | 108 ++++++++++++++++++++++++++++ 6 files changed, 205 insertions(+) create mode 100644 .dockerignore create mode 100644 deploy/docker-compose.review.yml create mode 100644 deploy/importarr.review.env.example create mode 100755 deploy/review/review.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..63046b1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.venv +.review-data +deploy/importarr.review.env +.pytest_cache +.mypy_cache +.ruff_cache +.tox +**/__pycache__ +**/*.pyc +**/*.pyo +*.egg-info +.coverage +htmlcov diff --git a/.gitignore b/.gitignore index 294772f..0c0cbb4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ __pycache__/ *.db *.partial AGENTS.local.md +.review-data/ +deploy/importarr.review.env diff --git a/README.md b/README.md index 7ffe7c5..7b44089 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,42 @@ pytest uvicorn importarr.main:app --reload ``` +### Persistent local review environment + +The review Compose stack builds the current working tree, including uncommitted +UI/API changes, and stays running for pre-commit or pre-push inspection. It is +separate from production: the web port is bound to localhost by default, state and +sample media live under the ignored `.review-data/` directory, external Arr/SAB +services are not required, and service-control/update commands are safe no-ops. + +```sh +deploy/review/review.sh up # build current files and start in background +deploy/review/review.sh update # rebuild changed files and recreate as needed +deploy/review/review.sh status +deploy/review/review.sh logs # follow logs; Ctrl-C leaves the stack running +deploy/review/review.sh stop # stop containers, preserving them and data +deploy/review/review.sh down # remove containers/network, preserving data +deploy/review/review.sh reset # remove stack and all local review data +``` + +On first use the wrapper copies `deploy/importarr.review.env.example` to the +ignored `deploy/importarr.review.env`. Adjust `REVIEW_PORT` there if port 18765 +is occupied, then review `http://127.0.0.1:18765/`. For review from a trusted +internal network, set `REVIEW_BIND_ADDRESS` to the host's LAN address and use +that address in the URL. Do not use `0.0.0.0` or expose this review stack to an +untrusted network; the safe default is `127.0.0.1`. To exercise imports without +real integrations, place disposable folders in `.review-data/downloads/`; movie +and TV destinations are `.review-data/movies/` and `.review-data/tv/`. +Each checkout gets its own Compose project and image name, so worktrees do not +replace each other's containers or images. Read-only and cleanup commands do not +create the local env file when it is absent. + +Agent workflow: run tests, run `update`, confirm `status` reports healthy, and +leave the stack running for the reviewer. Reviewer workflow: inspect the UI and +API, use `logs` when needed, and use `down` after review (or `reset` when the +saved review state is no longer useful). Re-run `update` after every working-tree +change that should be reviewed. + ## Operations Importarr intentionally does not document private deployment topology, hostnames, reverse proxies, monitoring, backups, or operator workflows in this repository. Keep those details in your own ops runbooks. diff --git a/deploy/docker-compose.review.yml b/deploy/docker-compose.review.yml new file mode 100644 index 0000000..de1c424 --- /dev/null +++ b/deploy/docker-compose.review.yml @@ -0,0 +1,22 @@ +services: + importarr: + build: + context: .. + dockerfile: Dockerfile + image: ${REVIEW_IMAGE:-importarr-review:local} + env_file: + - ${REVIEW_SERVICE_ENV_FILE:-importarr.review.env.example} + ports: + - "${REVIEW_BIND_ADDRESS:-127.0.0.1}:${REVIEW_PORT:-18765}:8765" + volumes: + - ../.review-data/config:/config + - ../.review-data/downloads:/data/downloads/manual + - ../.review-data/movies:/data/movies + - ../.review-data/tv:/data/tv + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health', timeout=2)"] + interval: 10s + timeout: 3s + retries: 6 + start_period: 5s diff --git a/deploy/importarr.review.env.example b/deploy/importarr.review.env.example new file mode 100644 index 0000000..b0c23f3 --- /dev/null +++ b/deploy/importarr.review.env.example @@ -0,0 +1,23 @@ +# Copied to importarr.review.env by deploy/review/review.sh. No secrets or +# external services are required for review. +REVIEW_PORT=18765 +# Keep this on loopback unless review access from a trusted network is needed. +REVIEW_BIND_ADDRESS=127.0.0.1 +IMPORTARR_BIND_HOST=0.0.0.0 +IMPORTARR_BIND_PORT=8765 +IMPORTARR_STATE_PATH=/config/importarr.db +IMPORTARR_DOWNLOAD_ROOT=/data/downloads/manual +IMPORTARR_MOVIES_ROOT=/data/movies +IMPORTARR_TV_ROOT=/data/tv +IMPORTARR_SAB_URL=http://127.0.0.1:9 +IMPORTARR_SAB_CATEGORY=review +IMPORTARR_POLL_SECONDS=3600 +IMPORTARR_LOG_LEVEL=info + +# UI control operations must not control host services or update this checkout. +IMPORTARR_START_COMMAND=/bin/true +IMPORTARR_STOP_COMMAND=/bin/true +IMPORTARR_RESTART_COMMAND=/bin/true +IMPORTARR_UPDATE_COMMAND=/bin/true +IMPORTARR_UPDATE_RELEASE_URL=http://127.0.0.1:9/releases/latest +IMPORTARR_UPDATE_CHECK_TIMEOUT_SECONDS=1 diff --git a/deploy/review/review.sh b/deploy/review/review.sh new file mode 100755 index 0000000..583dc71 --- /dev/null +++ b/deploy/review/review.sh @@ -0,0 +1,108 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(CDPATH= cd -- "$script_dir/../.." && pwd) +compose_file="$repo_dir/deploy/docker-compose.review.yml" +env_file="$repo_dir/deploy/importarr.review.env" +env_example="$repo_dir/deploy/importarr.review.env.example" +data_dir="$repo_dir/.review-data" +repo_name=$(printf '%s' "${repo_dir##*/}" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9_-' '-') +repo_id=$(printf '%s' "$repo_dir" | cksum | awk '{print $1}') +project_name="importarr-review-${repo_name}-${repo_id}" +review_image="${project_name}-importarr:review" + +usage() { + echo "Usage: $0 {up|update|status|logs|stop|down|reset}" >&2 + exit 2 +} + +command -v docker >/dev/null 2>&1 || { + echo "docker is required" >&2 + exit 1 +} + +if docker info >/dev/null 2>&1; then + docker_with_sudo=false +elif command -v sudo >/dev/null 2>&1 && sudo -n docker info >/dev/null 2>&1; then + docker_with_sudo=true +else + echo "cannot access the Docker daemon with docker or sudo -n docker" >&2 + exit 1 +fi + +compose() { + selected_env_file=$env_example + if [ -f "$env_file" ]; then + selected_env_file=$env_file + fi + + if [ "$docker_with_sudo" = true ]; then + sudo -n env REVIEW_IMAGE="$review_image" REVIEW_SERVICE_ENV_FILE="$selected_env_file" \ + docker compose --project-name "$project_name" --project-directory "$repo_dir/deploy" \ + --env-file "$selected_env_file" -f "$compose_file" "$@" + else + REVIEW_IMAGE="$review_image" REVIEW_SERVICE_ENV_FILE="$selected_env_file" \ + docker compose --project-name "$project_name" --project-directory "$repo_dir/deploy" \ + --env-file "$selected_env_file" -f "$compose_file" "$@" + fi +} + +ensure_env_file() { + if [ ! -f "$env_file" ]; then + cp "$env_example" "$env_file" + echo "Created $env_file from the safe review defaults." + fi +} + +remove_data() { + if rm -rf "$data_dir" 2>/dev/null && [ ! -e "$data_dir" ]; then + return + fi + if [ "$docker_with_sudo" = true ]; then + sudo -n rm -rf "$data_dir" + else + echo "cannot remove root-owned review data without passwordless sudo: $data_dir" >&2 + exit 1 + fi +} + +case "${1:-}" in + up|update) + ensure_env_file + mkdir -p "$data_dir/config" "$data_dir/downloads" "$data_dir/movies" "$data_dir/tv" + compose up --detach --build --wait + ;; + status) + compose ps + container_id=$(compose ps --quiet importarr) + [ -n "$container_id" ] || { + echo "review service is not running" >&2 + exit 1 + } + health=$(if [ "$docker_with_sudo" = true ]; then + sudo -n docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" + else + docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" + fi) + [ "$health" = healthy ] || { + echo "review service is not healthy (status: $health)" >&2 + exit 1 + } + ;; + logs) + compose logs --follow --tail=200 + ;; + stop) + compose stop + ;; + down) + compose down --remove-orphans + ;; + reset) + compose down --remove-orphans + remove_data + echo "Removed review data: $data_dir" + ;; + *) usage ;; +esac