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. # nobody offers sits in the queue rather than failing.
# #
# The runner has to be in host mode on the machine that serves the panel: # 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 # it updates DEPLOY_PATH and restarts the service, and it must run as the
# deploy/heos-panel.service for the unit and the one sudoers line the # user that owns that directory.
# restart needs.
# #
# Every step here is plain shell, on purpose. actions/checkout is a # DEPLOY_PATH is a git checkout, cloned there ONCE by hand -- see
# JavaScript action, and a host-mode runner can only run those if node is # "Deploying from Gitea" in the README. This workflow only fast-forwards
# on its PATH -- swapping the clone below back to `uses: actions/checkout` # it, so the remote and whatever credentials reach it are set up a single
# brings back "Cannot find: node in PATH" on a runner without it. # time and stay put.
# #
# DEPLOY_PATH is also where you edit: an rsync --delete lands on top of # Every step is plain shell on purpose: actions/checkout is a JavaScript
# whatever is sitting there uncommitted, so commit before you push. # 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 name: Deploy HEOS panel
@@ -31,72 +31,62 @@ jobs:
DEPLOY_PATH: /var/www/html/heos DEPLOY_PATH: /var/www/html/heos
SERVICE: heos-panel SERVICE: heos-panel
PANEL_URL: http://127.0.0.1:5005/ # WEB_PORT in config.py PANEL_URL: http://127.0.0.1:5005/ # WEB_PORT in config.py
SRC: src # the clone, inside the workspace
steps: steps:
- name: Check runner tools - name: Check runner tools
run: | run: |
command -v git command -v git
command -v python3 command -v python3
command -v rsync
command -v curl command -v curl
- name: Check deploy path - name: Check the deploy path is ready
run: | run: |
test -d "$DEPLOY_PATH" if [ ! -d "$DEPLOY_PATH" ] || [ ! -w "$DEPLOY_PATH" ]; then
test -w "$DEPLOY_PATH" 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 - name: Check the restart is allowed without a password
run: sudo -n systemctl is-active "$SERVICE" || true run: sudo -n systemctl is-active "$SERVICE" || true
# A shallow clone of the pushed branch, taking the remote from the - name: Fast-forward the checkout
# 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
run: | run: |
rm -rf "$SRC" cd "$DEPLOY_PATH"
git init --quiet "$SRC" git fetch --prune origin
git -C "$SRC" remote add origin "$(git -C "$DEPLOY_PATH" remote get-url origin)" # --ff-only on purpose: if someone has edited a tracked file on the
git -C "$SRC" fetch --quiet --depth 1 origin "${GITHUB_REF_NAME:-main}" # box without committing it, this stops rather than throwing their
git -C "$SRC" checkout --quiet FETCH_HEAD # change away.
git -C "$SRC" --no-pager log -1 --oneline 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 - name: Install dependencies
working-directory: src
run: | run: |
python3 -m venv .venv-ci cd "$DEPLOY_PATH"
.venv-ci/bin/pip install --quiet --upgrade pip test -d .venv || python3 -m venv .venv
.venv-ci/bin/pip install --quiet -r requirements.txt .venv/bin/pip install --quiet -r requirements.txt
# Runs against the fake HEOS and AVR servers in tests/fakes.py, so it # 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 - 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: | run: |
rsync -azc --no-times --delete \ cd "$DEPLOY_PATH"
--exclude "/.git/" \ .venv/bin/python -m unittest discover -s tests -t . --verbose
--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 - name: Restart
run: sudo systemctl restart "$SERVICE" 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 ## Deploying from Gitea
`.gitea/workflows/deploy.yml` runs the tests on every push to `main`, then `.gitea/workflows/deploy.yml` fast-forwards the checkout on the server,
rsyncs the tree into place, installs anything new from `requirements.txt`, installs anything new from `requirements.txt`, runs the tests, restarts the
restarts the service and waits for the panel to answer again. service and waits for the panel to answer again.
It needs a runner **in host mode on the machine that serves the panel**, ### Once, by hand on the server
registered with the label `heos` (`runs-on:` must match, or the job queues
forever), running as the user that owns the directory.
Every step is plain shell. `actions/checkout` is a JavaScript action, and a The workflow only ever *updates* a checkout. It never creates one, so the
host-mode runner can only run those with `node` on its PATH — swapping the remote and whatever credentials reach it are set up a single time and stay
clone back for it brings back `Cannot find: node in PATH`. The clone takes put. As the user the runner runs as:
its URL from the live checkout's own remote, so no URL or token is written
down here.
Restarting needs one sudoers line:
```bash ```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 tee /etc/sudoers.d/heos-panel
sudo chmod 440 /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 Edit `User=` and the paths in the unit if the panel lives somewhere else or
learned stereo-pair membership survive a deploy. It does *not* exclude runs as someone else. Until that clone exists the workflow stops on its
`config.py`: your device names live in git, so commit changes to them first real step and prints these commands back at you.
rather than editing the deployed copy. And since the deploy path is also
where you edit, `--delete` lands on top of anything uncommitted sitting ### The runner
there.
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 ## Behind a reverse proxy, at /heos
+1 -1
View File
@@ -66,4 +66,4 @@ VOLUME_STEP = 5
AVR_INPUT_CODES = [] AVR_INPUT_CODES = []
# Shown as the app's name on the iOS home screen. # Shown as the app's name on the iOS home screen.
APP_NAME = "HEOS" APP_NAME = "Heos"