infra: publish-apk.sh --deploy copies the APK to the Pi and checks /apk/latest (D-139)

A dedicated forced-command key (scp -t into data/apk only) copies
kvitt-<code>.apk then latest.json with legacy scp (-O), host key pinned in
infra/secrets/pi_known_hosts on first use, then curls
https://$KVITT_PUBLIC_HOST/apk/latest with X-Kvitt-App (fed on stdin) and
requires the new version_code. Without --deploy nothing changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-21 20:47:47 +02:00
parent 44c8cbb771
commit 70cb25c5d0
2 changed files with 95 additions and 9 deletions
+30 -4
View File
@@ -316,9 +316,34 @@ tags of the Gitea workflow (`v1.0.N`) are separate and say nothing about the APK
builds `:app:assembleRelease` through `tools/gradle-serial.sh` (JDK 25 on `PATH`), refuses an unsigned APK, and
writes into `infra/pi/out/apk/`: `kvitt-<version_code>.apk`, `kvitt-<version_code>.mapping.txt` (the R8 mapping,
for reading stack traces; never served) and `latest.json` (temp file, then rename); it keeps the newest 5 APKs
with their mappings. Then the owner, on the Mac, copies the APK first and `latest.json` last into the server's
`data/apk/` (the server reads `latest.json` on every `GET /apk/latest`, so it must never name a file that is not
there yet):
with their mappings.
**Deploy (D-139):** `infra/publish-apk.sh --deploy` then copies the APK first and `latest.json` last into the server's
`data/apk/` on the Kvitt Pi (the server reads `latest.json` on every `GET /apk/latest`, so it must never name a file
that is not there yet) and checks that `https://$KVITT_PUBLIC_HOST/apk/latest` returns the new `version_code`, with
the header `X-Kvitt-App` from `kvitt.appKey` in `android/local.properties` (the NPM app gate, D-126; the key is fed
to curl on stdin and never printed), so it works from anywhere. It prints `Pi serves version_code N` or fails. The
copy is, per file:
```bash
scp -O -i infra/secrets/pi_deploy_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes \
-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=infra/secrets/pi_known_hosts \
-P 22 <file> jacobhagstedt@gitea.local:
```
Host, user and port are constants at the top of the script (`.env` holds no SSH details, D-125). Without the key file
`--deploy` refuses to start; without `kvitt.appKey` it refuses too. The Pi's host key is recorded on first use in
`infra/secrets/pi_known_hosts` (git-ignored); if the Pi is reinstalled, delete that file.
**The deploy key.** `infra/secrets/pi_deploy_ed25519` (git-ignored, on the Mac only) is a dedicated key whose public
half is one line in `~/.ssh/authorized_keys` of `jacobhagstedt` on the Kvitt Pi, prefixed with
`command="scp -t /home/jacobhagstedt/kvitt/data/apk",restrict`: whatever the client asks for, the Pi runs only that
`scp` receiver, so the key can write files into `data/apk/` and nothing else (no shell, no forwarding, no pty; the
target path given on the Mac is ignored, which is why the command ends in a bare `:` and needs `-O`, the legacy scp
protocol a forced `scp -t` speaks). To revoke it, delete that line from `~/.ssh/authorized_keys` on the Pi.
**Manual fallback** (no key, or the deploy fails): without `--deploy` the script only stages, and the owner copies
from the Mac, the APK first and `latest.json` last:
```bash
cd /Users/jhag/projects/kvitt
@@ -328,7 +353,8 @@ curl -s http://<kvitt-pi-address>:8080/apk/latest # shows the new version_co
```
The mapping files stay on the Mac. Older APKs on the Pi are removed by hand when wanted
(`ssh jacobhagstedt@<kvitt-pi-address> ls kvitt/data/apk`, then `rm` the old `kvitt-<n>.apk` files; about 10 MB each).
(`ssh jacobhagstedt@<kvitt-pi-address> ls kvitt/data/apk`, then `rm` the old `kvitt-<n>.apk` files; about 10 MB each);
the deploy key cannot list or delete.
## Pi: what the owner does