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