From 6fcc345a26a95cb3d29d2bc1f4479f5e06d7a85d Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sat, 19 Sep 2026 21:13:00 +0200 Subject: [PATCH] Only rebuild the venv when the deployed commit changes The previous fix made the install convergent but not idempotent: it keyed "changed" off uv printing "Installed", and uv prints that every single run. musicmouse is a local path dependency, so uv rebuilds and reinstalls it every time regardless. Every ansible run therefore restarted the service and made it rescan the whole library for nothing. Decide from the device instead: the commit the checkout landed on, recorded in the venv, plus whether the venv can import the package at all. That keeps the convergence the handler lacked - an empty venv reinstalls even when the commit matches - without touching anything on a run where nothing moved. Co-Authored-By: Claude Opus 5 --- roles/pi_musicmouse/defaults/main.yml | 4 +++ roles/pi_musicmouse/tasks/main.yml | 42 ++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/roles/pi_musicmouse/defaults/main.yml b/roles/pi_musicmouse/defaults/main.yml index 72ea230..c44756c 100644 --- a/roles/pi_musicmouse/defaults/main.yml +++ b/roles/pi_musicmouse/defaults/main.yml @@ -43,3 +43,7 @@ pi_musicmouse_force_config: false # section will not start without it - curriculum_file is validated as must-exist. Set to # "" for a host whose config has no tippen section. pi_musicmouse_curriculum_file: "tippen-curriculum.yml" + +# The commit the checkout task actually landed on, used to decide whether the venv +# needs rebuilding. Empty under --check, where the git task reports without writing. +pi_musicmouse_commit: "{{ pi_musicmouse_checkout.after | default('') }}" diff --git a/roles/pi_musicmouse/tasks/main.yml b/roles/pi_musicmouse/tasks/main.yml index 1b18a88..8fdf7b2 100644 --- a/roles/pi_musicmouse/tasks/main.yml +++ b/roles/pi_musicmouse/tasks/main.yml @@ -51,6 +51,7 @@ version: "{{ musicmouse_version }}" accept_hostkey: true force: true + register: pi_musicmouse_checkout - name: Check the checkout has the layout this role knows ansible.builtin.stat: @@ -77,18 +78,45 @@ cmd: "/usr/local/bin/uv venv --python {{ pi_musicmouse_python_version }} /opt/musicmouse/.venv" creates: /opt/musicmouse/.venv/bin/python +# Installing the backend is deliberately not a handler hanging off the checkout. +# Tied to the checkout, one failed run strands the device: the notification is dropped +# when the play fails, the next run finds the checkout already current and notifies +# nothing, and the venv stays empty while the service crash-loops on +# ModuleNotFoundError. Instead, decide from what is actually on the device - the commit +# it has checked out, and whether the venv can import the package at all. +# +# uv's own output cannot answer this: `musicmouse` is a local path dependency, so every +# run rebuilds and reinstalls it and always prints "Installed 1 package". Keying off +# that restarts the service on every run and rescans the whole library for nothing. +- name: Read which commit the venv was built from + ansible.builtin.command: + cmd: cat /opt/musicmouse/.venv/.installed-commit + register: pi_musicmouse_stamp + changed_when: false + failed_when: false + +- name: Check the venv can actually import the backend + ansible.builtin.command: + cmd: /opt/musicmouse/.venv/bin/python -c "import musicmouse" + register: pi_musicmouse_importable + changed_when: false + failed_when: false + - name: Install the backend into the venv - # Every run, not on a checkout change via a handler. Tied to the checkout, one failed - # run strands the device: the notification is dropped when the play fails, the next - # run finds the checkout already current and notifies nothing, and the venv stays - # empty while the service crash-loops on ModuleNotFoundError. uv is fast when there - # is nothing to do, and its output says whether there was. ansible.builtin.command: cmd: "/usr/local/bin/uv pip install --python /opt/musicmouse/.venv/bin/python /opt/musicmouse/python-backend" - register: pi_musicmouse_install - changed_when: "'Installed' in pi_musicmouse_install.stderr" + when: >- + pi_musicmouse_importable.rc != 0 + or pi_musicmouse_stamp.stdout | trim != pi_musicmouse_commit + changed_when: true # guarded by `when`, so reaching this means it installed something notify: Restart musicmouse +- name: Record which commit the venv was built from + ansible.builtin.copy: + content: "{{ pi_musicmouse_commit }}\n" + dest: /opt/musicmouse/.venv/.installed-commit + mode: "0644" + - name: Create media directory ansible.builtin.file: path: /media/musicmouse