From 467cae4dd54f442b3af808826a4ccb358da1e516 Mon Sep 17 00:00:00 2001 From: Franzz Date: Mon, 14 Sep 2026 22:56:21 +0200 Subject: [PATCH] Add a Gitea deploy workflow and the systemd unit it restarts Shaped after the Livetrail workflow: one job on a project-labelled host runner, preflight checks, rsync from the runner's checkout. rsync rather than a pull in the live directory, so the deploy needs no credentials of its own for git:. The tests run first and gate the rest, .venv and members.json are excluded so the runtime and the learned stereo pair survive, and the last step waits for the panel to answer -- the home page renders from config alone, so it says the app came back up without waiting on the speakers. The unit carries the real paths and user, which the README's sketch of it never did. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/deploy.yml | 97 +++++++++++++++++++++++++++++++++++++ README.md | 45 +++++++++++------ deploy/heos-panel.service | 31 ++++++++++++ 3 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 deploy/heos-panel.service diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..eeedb2a --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,97 @@ +# runs-on must match the label the runner was registered with -- `heos` +# here, the way Livetrail uses `livetrail`. A job asking for a label +# nobody offers sits in the queue rather than failing. +# +# The runner has to be in host mode on the machine that serves the panel: +# it writes into DEPLOY_PATH and restarts the service. See +# deploy/heos-panel.service for the unit and the one sudoers line the +# restart needs. +# +# DEPLOY_PATH is also where you edit: an rsync --delete lands on top of +# whatever is sitting there uncommitted, so commit before you push. + +name: Deploy HEOS panel + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: heos + + env: + DEPLOY_PATH: /var/www/html/heos + SERVICE: heos-panel + PANEL_URL: http://127.0.0.1:5005/ # WEB_PORT in config.py + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check runner tools + run: | + command -v python3 + command -v rsync + command -v curl + + - name: Check deploy path + run: | + test -d "$DEPLOY_PATH" + test -w "$DEPLOY_PATH" + + - name: Check the restart is allowed without a password + run: sudo -n systemctl is-active "$SERVICE" || true + + # A throwaway virtualenv in the workspace: the one under + # $DEPLOY_PATH/.venv is what the running panel imports from, and a + # test run has no business touching it. + - name: Install dependencies + run: | + python3 -m venv .venv-ci + .venv-ci/bin/pip install --quiet --upgrade pip + .venv-ci/bin/pip install --quiet -r requirements.txt + + # Runs against the fake HEOS and AVR servers in tests/fakes.py, so it + # needs no speakers and touches nothing on the network. + - name: Run tests + run: .venv-ci/bin/python -m unittest discover -s tests -t . --verbose + + - name: Deploy to production + run: | + rsync -azc --no-times --delete \ + --exclude "/.git/" \ + --exclude "/.gitea/" \ + --exclude "/.venv/" \ + --exclude "/.venv-ci/" \ + --exclude "/members.json" \ + --exclude "__pycache__/" \ + ./ "$DEPLOY_PATH/" + + # members.json is the stereo pair's learned membership and .venv is + # the runtime -- both are excluded above, so --delete leaves them be. + + - name: Install runtime dependencies + run: | + test -d "$DEPLOY_PATH/.venv" || python3 -m venv "$DEPLOY_PATH/.venv" + "$DEPLOY_PATH/.venv/bin/pip" install --quiet -r "$DEPLOY_PATH/requirements.txt" + + - name: Restart + run: sudo systemctl restart "$SERVICE" + + - name: Wait for the panel to answer + run: | + # The home page renders from config alone, so this proves the app + # came back up without waiting on the speakers to reply. + for attempt in $(seq 1 20); do + if curl -fsS -o /dev/null "$PANEL_URL"; then + echo "panel is up after ${attempt}s" + exit 0 + fi + sleep 1 + done + echo "panel did not come back -- last of its log:" + sudo systemctl status "$SERVICE" --no-pager --lines 30 || true + exit 1 diff --git a/README.md b/README.md index 2488232..10c6448 100644 --- a/README.md +++ b/README.md @@ -84,26 +84,41 @@ if you ever put it behind a domain name, give it HTTPS. ## Run it as a service -```ini -# /etc/systemd/system/heos-panel.service -[Unit] -Description=HEOS panel -After=network-online.target - -[Service] -ExecStart=/home/pi/heos/.venv/bin/python /home/pi/heos/app.py -WorkingDirectory=/home/pi/heos -Restart=always -User=pi - -[Install] -WantedBy=multi-user.target -``` +`deploy/heos-panel.service` runs the panel out of its own virtualenv and +restarts it if it dies: ```bash +sudo cp deploy/heos-panel.service /etc/systemd/system/ +sudo systemctl daemon-reload sudo systemctl enable --now heos-panel ``` +Edit `User=` and the paths in it if you keep the panel somewhere else. + +## Deploying from Gitea + +`.gitea/workflows/deploy.yml` runs the tests on every push to `main`, then +rsyncs the tree into place, installs anything new from `requirements.txt`, +restarts the service and waits for the panel to answer again. + +It needs a runner **in host mode on the machine that serves the panel**, +registered with the label `heos` (`runs-on:` must match, or the job queues +forever), running as the user that owns the directory. Restarting needs +one sudoers line: + +```bash +echo 'franzz ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart heos-panel' \ + | sudo tee /etc/sudoers.d/heos-panel +sudo chmod 440 /etc/sudoers.d/heos-panel +``` + +The rsync excludes `.venv` and `members.json`, so the runtime and the +learned stereo-pair membership survive a deploy. It does *not* exclude +`config.py`: your device names live in git, so commit changes to them +rather than editing the deployed copy. And since the deploy path is also +where you edit, `--delete` lands on top of anything uncommitted sitting +there. + ## Behind a reverse proxy, at /heos `deploy/heos.conf` reverse-proxies `/heos` to the panel with Apache: diff --git a/deploy/heos-panel.service b/deploy/heos-panel.service new file mode 100644 index 0000000..d555422 --- /dev/null +++ b/deploy/heos-panel.service @@ -0,0 +1,31 @@ +# The HEOS panel as a service. +# +# sudo cp /var/www/html/heos/deploy/heos-panel.service /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable --now heos-panel +# +# So the deploy workflow can restart it without a password prompt: +# +# echo 'franzz ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart heos-panel' \ +# | sudo tee /etc/sudoers.d/heos-panel +# sudo chmod 440 /etc/sudoers.d/heos-panel +# +# (that user is whoever the Gitea runner runs as -- see .gitea/workflows/deploy.yml) + +[Unit] +Description=HEOS panel +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=franzz +Group=www-data +WorkingDirectory=/var/www/html/heos +ExecStart=/var/www/html/heos/.venv/bin/python /var/www/html/heos/app.py +# Add --host 127.0.0.1 above to allow only the reverse proxy in. +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target