diff --git a/AGENTS.md b/AGENTS.md
index 23cb3f9..f239836 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,3 +5,7 @@ The Importarr review site at must always reflect th
- After making local code changes, run `deploy/review/review.sh update` from the repository root.
- Before finishing, verify that `http://172.20.30.35:18765/` responds successfully.
- If deployment or URL verification fails, report the failure clearly; do not claim the review site is current.
+
+## Release workflow
+
+Generic requests such as "release app", "release to live", or "release importarr" mean the complete release-to-live flow: verify a clean release candidate, run tests, commit the intended changes, create and push a SemVer tag and repository release, call the live bearer's protected `POST /api/control/update?expected_tag=` with the required exact release tag, then verify health and that the reported installed version matches the release. Follow any host-local operational runbook for credentials and live URLs; never put tokens or other secrets in this repository, commands shown in logs, release notes, or commits.
diff --git a/Dockerfile b/Dockerfile
index 723a16b..365a470 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ WORKDIR /app
COPY pyproject.toml README.md LICENSE ./
COPY importarr ./importarr
COPY --from=frontend /build/importarr/static ./importarr/static
-ARG IMPORTARR_VERSION=0.1.0
+ARG IMPORTARR_VERSION=0.1.1
ARG IMPORTARR_BUILD_DATE=unknown
ARG IMPORTARR_GIT_SHA=unknown
ENV IMPORTARR_VERSION=$IMPORTARR_VERSION \
diff --git a/Makefile b/Makefile
index 2e13fcf..564e83c 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@ SERVICE ?= importarr.service
IMPORTARR_PREFIX ?= /opt/importarr
IMPORTARR_REPO_DIR ?= $(CURDIR)
IMPORTARR_URL ?= http://127.0.0.1:8765
+RELEASE ?= vMAJOR.MINOR.PATCH
.PHONY: test install-systemd install-from-repo repo-upgrade verify
@@ -17,7 +18,7 @@ install-from-repo:
sudo -n $(IMPORTARR_PREFIX)/venv/bin/pip install --upgrade $(IMPORTARR_REPO_DIR)
repo-upgrade:
- sudo -n IMPORTARR_PREFIX=$(IMPORTARR_PREFIX) IMPORTARR_REPO_DIR=$(IMPORTARR_REPO_DIR) sh deploy/repo-upgrade.sh
+ sudo -n IMPORTARR_PREFIX=$(IMPORTARR_PREFIX) IMPORTARR_REPO_DIR=$(IMPORTARR_REPO_DIR) sh deploy/repo-upgrade.sh $(RELEASE)
verify:
curl -fsS $(IMPORTARR_URL)/health
diff --git a/README.md b/README.md
index b5acc66..26c6814 100644
--- a/README.md
+++ b/README.md
@@ -38,12 +38,12 @@ The installer creates the `importarr` system user when needed, installs a virtua
For a machine that should stay current with the repository, use the installed repo-upgrade helper:
```sh
-sudo -n sh /opt/importarr/repo-upgrade.sh
+sudo -n sh /opt/importarr/repo-upgrade.sh v1.2.3
```
-The helper refuses to run when the checkout has uncommitted changes, then performs `git pull --ff-only`, reinstalls the package from the repo, restarts `importarr.service`, and prints service status. Use it after changes have been committed and pushed to `main`.
+The helper requires the intended release tag and a clean Git checkout at `/opt/importarr/repo` (override with `IMPORTARR_REPO_DIR` only for a nonstandard installation). It fetches tags, checks out that exact tag in detached-HEAD state, reinstalls the package, records tag/SHA provenance, and restarts `importarr.service`; it never installs an arbitrary branch head.
-Installed deployments can expose the same operation through the authenticated API. `GET /api/control/update-check` queries the latest release from `IMPORTARR_UPDATE_RELEASE_URL` (default: this repository's Gitea latest-release API) and compares it with the running `IMPORTARR_VERSION`. `POST /api/control/update` performs the same check and only runs the update command when a newer release tag exists. Configure `IMPORTARR_UPDATE_COMMAND` when the default `sh deploy/repo-upgrade.sh` is not correct for the service working directory. The web UI Start, Stop, and Restart controls target `manual-media-import.service` by default; configure `IMPORTARR_START_COMMAND`, `IMPORTARR_STOP_COMMAND`, or `IMPORTARR_RESTART_COMMAND` when those defaults need a wrapper such as sudo.
+Installed deployments can expose the same operation through the authenticated API. `GET /api/control/update-check` queries the latest release from `IMPORTARR_UPDATE_RELEASE_URL` (default: this repository's Gitea latest-release API) and compares it with the installed release recorded in `/opt/importarr/build.env`. `POST /api/control/update?expected_tag=v1.2.3` verifies that the latest release is the caller's expected tag, appends that exact tag to the configured command, and schedules it in a detached process after returning. A mismatch returns `409` without scheduling an upgrade. The upgrade helper holds an exclusive lock across fetch, checkout, install, provenance update, and service restart, so concurrent requests cannot overlap. Poll `/health` and `update-check` until the new process is healthy and reports the expected tag. The systemd default is the working-directory-independent `/bin/sh /opt/importarr/repo-upgrade.sh`; configure `IMPORTARR_UPDATE_COMMAND` only when the helper is installed elsewhere. The web UI Start, Stop, and Restart controls target `manual-media-import.service` by default; configure `IMPORTARR_START_COMMAND`, `IMPORTARR_STOP_COMMAND`, or `IMPORTARR_RESTART_COMMAND` when those defaults need a wrapper such as sudo.
Release-worthy changes should be committed, tagged with SemVer (`v0.1.1`, `v0.2.0`, ...), pushed with tags, then installed from the tagged checkout or artifact.
@@ -53,7 +53,7 @@ Release-worthy changes should be committed, tagged with SemVer (`v0.1.1`, `v0.2.
2. Set its completed folder to the same path mounted as `IMPORTARR_DOWNLOAD_ROOT`.
3. Set `IMPORTARR_SAB_URL` and `IMPORTARR_SAB_API_KEY_FILE` or `IMPORTARR_SAB_API_KEY`.
4. Mount/configure `IMPORTARR_MOVIES_ROOT` and `IMPORTARR_TV_ROOT` read/write.
-5. Set `IMPORTARR_AUTH_TOKEN` unless write endpoints are protected by a reverse proxy.
+5. Set `IMPORTARR_AUTH_TOKEN_FILE` or `IMPORTARR_AUTH_TOKEN` for every non-loopback bind. Importarr fails closed on write/control endpoints when a non-loopback service has no token; tokenless local development remains available on `127.0.0.1`, `::1`, or `localhost`.
6. Check `GET /health`, then inspect `/api/preview` before running imports.
## Safety model
@@ -79,7 +79,7 @@ Release-worthy changes should be committed, tagged with SemVer (`v0.1.1`, `v0.2.
- `POST /api/control/cancel-current`
- `POST /api/control/restart`
- `GET /api/control/update-check`
-- `POST /api/control/update`
+- `POST /api/control/update?expected_tag=v1.2.3` (`expected_tag` is required)
- `POST /api/queue-items/{id}/action` with `{ "action": "retry|ignore|remove" }`
- `POST /api/import/run-now`
diff --git a/deploy/docker-compose.example.yml b/deploy/docker-compose.example.yml
index 8b6e200..2164da7 100644
--- a/deploy/docker-compose.example.yml
+++ b/deploy/docker-compose.example.yml
@@ -1,6 +1,6 @@
services:
importarr:
- image: ghcr.io/OWNER/importarr:0.1.0
+ image: ghcr.io/OWNER/importarr:0.1.1
env_file: importarr.env
ports:
- "8765:8765"
diff --git a/deploy/importarr.env.example b/deploy/importarr.env.example
index b15b06f..34ebddf 100644
--- a/deploy/importarr.env.example
+++ b/deploy/importarr.env.example
@@ -17,3 +17,5 @@ IMPORTARR_BIND_HOST=0.0.0.0
IMPORTARR_BIND_PORT=8765
IMPORTARR_POLL_SECONDS=60
# IMPORTARR_REPO_DIR=/path/to/importarr
+# Override only when the installed helper is not at the standard systemd path.
+# IMPORTARR_UPDATE_COMMAND=/bin/sh /opt/importarr/repo-upgrade.sh
diff --git a/deploy/repo-upgrade.sh b/deploy/repo-upgrade.sh
index 691771d..45a83ff 100644
--- a/deploy/repo-upgrade.sh
+++ b/deploy/repo-upgrade.sh
@@ -2,7 +2,7 @@
set -eu
if [ "$(id -u)" -ne 0 ]; then
- echo "Run as root: sudo sh /opt/importarr/repo-upgrade.sh" >&2
+ echo "Run as root: sudo sh /opt/importarr/repo-upgrade.sh vMAJOR.MINOR.PATCH" >&2
exit 1
fi
@@ -13,9 +13,22 @@ if [ -f "$ENV_FILE" ]; then
fi
PREFIX=${IMPORTARR_PREFIX:-/opt/importarr}
-REPO_DIR=${IMPORTARR_REPO_DIR:-$(pwd)}
+REPO_DIR=${IMPORTARR_REPO_DIR:-$PREFIX/repo}
SERVICE=${IMPORTARR_SERVICE:-importarr.service}
VENV=${IMPORTARR_VENV:-$PREFIX/venv}
+RELEASE_TAG=${1:-}
+
+if [ -z "$RELEASE_TAG" ]; then
+ echo "Usage: $0 vMAJOR.MINOR.PATCH" >&2
+ exit 2
+fi
+
+LOCK_FILE=${IMPORTARR_UPGRADE_LOCK_FILE:-$PREFIX/repo-upgrade.lock}
+exec 9>"$LOCK_FILE"
+if ! flock -n 9; then
+ echo "Refusing to upgrade: another Importarr upgrade is already running." >&2
+ exit 1
+fi
if [ ! -d "$REPO_DIR/.git" ]; then
echo "Importarr repo not found at $REPO_DIR" >&2
@@ -29,14 +42,22 @@ if [ -n "$(git status --porcelain)" ]; then
exit 1
fi
-git fetch --prune origin
-git pull --ff-only
+git fetch --prune --tags origin
+if ! git rev-parse --verify --quiet "refs/tags/$RELEASE_TAG" >/dev/null; then
+ echo "Release tag not found: $RELEASE_TAG" >&2
+ exit 1
+fi
+git checkout --detach "$RELEASE_TAG"
+test "$(git describe --tags --exact-match HEAD)" = "$RELEASE_TAG"
"$VENV/bin/pip" install --upgrade "$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" <> /etc/importarr/importarr.env
-fi
install -m 0644 "$REPO_DIR/deploy/importarr.service" /etc/systemd/system/importarr.service
install -m 0644 "$REPO_DIR/deploy/manual-media-import.service" /etc/systemd/system/manual-media-import.service
install -m 0644 "$REPO_DIR/deploy/manual-media-import.timer" /etc/systemd/system/manual-media-import.timer
install -m 0644 "$REPO_DIR/deploy/manual-media-import-failure.service" /etc/systemd/system/manual-media-import-failure.service
install -m 0755 "$REPO_DIR/deploy/repo-upgrade.sh" /opt/importarr/repo-upgrade.sh
+if [ "$REPO_DIR" != /opt/importarr/repo ]; then
+ if [ -e /opt/importarr/repo ] && [ ! -L /opt/importarr/repo ]; then
+ echo "Refusing to replace existing /opt/importarr/repo; move it or set IMPORTARR_REPO_DIR explicitly." >&2
+ exit 1
+ fi
+ ln -sfn "$REPO_DIR" /opt/importarr/repo
+fi
GIT_SHA="$(git -C "$REPO_DIR" rev-parse --short=12 HEAD 2>/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 <