Simplify deployment
Deploy HEOS panel / deploy (push) Failing after 0s

This commit is contained in:
2026-09-14 23:48:51 +02:00
parent 4cff820070
commit a7a8ab6f4e
3 changed files with 83 additions and 75 deletions
+44 -54
View File
@@ -3,17 +3,17 @@
# 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.
# it updates DEPLOY_PATH and restarts the service, and it must run as the
# user that owns that directory.
#
# Every step here is plain shell, on purpose. actions/checkout is a
# JavaScript action, and a host-mode runner can only run those if node is
# on its PATH -- swapping the clone below back to `uses: actions/checkout`
# brings back "Cannot find: node in PATH" on a runner without it.
# DEPLOY_PATH is a git checkout, cloned there ONCE by hand -- see
# "Deploying from Gitea" in the README. This workflow only fast-forwards
# it, so the remote and whatever credentials reach it are set up a single
# time and stay put.
#
# DEPLOY_PATH is also where you edit: an rsync --delete lands on top of
# whatever is sitting there uncommitted, so commit before you push.
# Every step is plain shell on purpose: actions/checkout is a JavaScript
# action, and a host-mode runner can only run those with node on its PATH,
# failing with "Cannot find: node in PATH" without one.
name: Deploy HEOS panel
@@ -31,72 +31,62 @@ jobs:
DEPLOY_PATH: /var/www/html/heos
SERVICE: heos-panel
PANEL_URL: http://127.0.0.1:5005/ # WEB_PORT in config.py
SRC: src # the clone, inside the workspace
steps:
- name: Check runner tools
run: |
command -v git
command -v python3
command -v rsync
command -v curl
- name: Check deploy path
- name: Check the deploy path is ready
run: |
test -d "$DEPLOY_PATH"
test -w "$DEPLOY_PATH"
if [ ! -d "$DEPLOY_PATH" ] || [ ! -w "$DEPLOY_PATH" ]; then
echo "$DEPLOY_PATH is missing, or not writable by $(id -un)."
exit 1
fi
if [ ! -d "$DEPLOY_PATH/.git" ]; then
echo "$DEPLOY_PATH is not a checkout yet. Once, on this machine,"
echo "as $(id -un):"
echo
echo " git clone <this repo> $DEPLOY_PATH"
echo " cd $DEPLOY_PATH"
echo " python3 -m venv .venv"
echo " .venv/bin/pip install -r requirements.txt"
echo " sudo cp deploy/heos-panel.service /etc/systemd/system/"
echo " sudo systemctl enable --now $SERVICE"
echo
echo "Then edit config.py for this house and push again."
exit 1
fi
- name: Check the restart is allowed without a password
run: sudo -n systemctl is-active "$SERVICE" || true
# A shallow clone of the pushed branch, taking the remote from the
# live checkout so there is no URL or token written down here. It
# works because the runner runs as the user that owns that checkout,
# which is the same reason it can write to DEPLOY_PATH at all.
- name: Checkout
- name: Fast-forward the checkout
run: |
rm -rf "$SRC"
git init --quiet "$SRC"
git -C "$SRC" remote add origin "$(git -C "$DEPLOY_PATH" remote get-url origin)"
git -C "$SRC" fetch --quiet --depth 1 origin "${GITHUB_REF_NAME:-main}"
git -C "$SRC" checkout --quiet FETCH_HEAD
git -C "$SRC" --no-pager log -1 --oneline
cd "$DEPLOY_PATH"
git fetch --prune origin
# --ff-only on purpose: if someone has edited a tracked file on the
# box without committing it, this stops rather than throwing their
# change away.
git merge --ff-only "origin/${GITHUB_REF_NAME:-main}"
git --no-pager log -1 --oneline
# 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
working-directory: src
run: |
python3 -m venv .venv-ci
.venv-ci/bin/pip install --quiet --upgrade pip
.venv-ci/bin/pip install --quiet -r requirements.txt
cd "$DEPLOY_PATH"
test -d .venv || python3 -m venv .venv
.venv/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.
# needs no speakers and touches nothing on the network. The new code
# is on disk by this point, but the running process is still the old
# one: a failure here stops the job before the restart below.
- name: Run tests
working-directory: src
run: .venv-ci/bin/python -m unittest discover -s tests -t . --verbose
- name: Deploy to production
working-directory: src
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"
cd "$DEPLOY_PATH"
.venv/bin/python -m unittest discover -s tests -t . --verbose
- name: Restart
run: sudo systemctl restart "$SERVICE"
+38 -20
View File
@@ -97,34 +97,52 @@ 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.
`.gitea/workflows/deploy.yml` fast-forwards the checkout on the server,
installs anything new from `requirements.txt`, runs the tests, 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.
### Once, by hand on the server
Every step is plain shell. `actions/checkout` is a JavaScript action, and a
host-mode runner can only run those with `node` on its PATH — swapping the
clone back for it brings back `Cannot find: node in PATH`. The clone takes
its URL from the live checkout's own remote, so no URL or token is written
down here.
Restarting needs one sudoers line:
The workflow only ever *updates* a checkout. It never creates one, so the
remote and whatever credentials reach it are set up a single time and stay
put. As the user the runner runs as:
```bash
echo 'franzz ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart heos-panel' \
git clone <this repo> /var/www/html/heos
cd /var/www/html/heos
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
sudo cp deploy/heos-panel.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now heos-panel
echo "$(id -un) 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.
Edit `User=` and the paths in the unit if the panel lives somewhere else or
runs as someone else. Until that clone exists the workflow stops on its
first real step and prints these commands back at you.
### The runner
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 checkout — it writes there, and it fetches with that
checkout's own git credentials.
Every step is plain shell. `actions/checkout` is a JavaScript action, and a
host-mode runner can only run those with `node` on its PATH; without one it
fails with `Cannot find: node in PATH`.
Nothing untracked is disturbed, so `.venv` and `members.json` — the runtime
and the stereo pair's learned membership — survive a deploy untouched. The
fast-forward is `--ff-only`, so a tracked file edited on the box without
being committed stops the deploy rather than being silently overwritten.
`config.py` is tracked: your device names live in git, so change them there
and push, rather than editing the deployed copy.
## Behind a reverse proxy, at /heos
+1 -1
View File
@@ -66,4 +66,4 @@ VOLUME_STEP = 5
AVR_INPUT_CODES = []
# Shown as the app's name on the iOS home screen.
APP_NAME = "HEOS"
APP_NAME = "Heos"