Add persistent local review stack

This commit is contained in:
2026-07-30 14:36:45 +02:00
parent c5cc0901c5
commit fb955adaf8
6 changed files with 205 additions and 0 deletions
+14
View File
@@ -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
+2
View File
@@ -6,3 +6,5 @@ __pycache__/
*.db *.db
*.partial *.partial
AGENTS.local.md AGENTS.local.md
.review-data/
deploy/importarr.review.env
+36
View File
@@ -95,6 +95,42 @@ pytest
uvicorn importarr.main:app --reload 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 ## 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. 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.
+22
View File
@@ -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
+23
View File
@@ -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
+108
View File
@@ -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