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:
+30
-4
@@ -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
|
||||
|
||||
|
||||
+65
-5
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the release APK on the Mac and stage it for the Pi (SPEC §10.6, D-100).
|
||||
# Usage, from the main checkout: infra/publish-apk.sh
|
||||
# Usage, from the main checkout: infra/publish-apk.sh [--deploy]
|
||||
# 1. refuses a dirty working tree (the version is derived from HEAD)
|
||||
# 2. version (SPEC §10.6): version_code = `git rev-list --count HEAD`, version_name = YYYY.MM.DD-<short sha> of
|
||||
# HEAD's commit date (Europe/Stockholm), passed as -Pkvitt.versionCode / -Pkvitt.versionName
|
||||
@@ -10,13 +10,47 @@
|
||||
# 5. writes into infra/pi/out/apk/: kvitt-<version_code>.apk, kvitt-<version_code>.mapping.txt (R8 mapping, for
|
||||
# reading stack traces; never served), and latest.json (written to a temp file, then renamed)
|
||||
# 6. keeps the newest 5 APKs (and their mappings) in infra/pi/out/apk/
|
||||
# Then the owner copies the APK and then latest.json into ~/kvitt/data/apk/ on the Pi with scp (infra/README.md).
|
||||
# Without --deploy the owner then copies the APK and then latest.json into ~/kvitt/data/apk/ on the Pi with scp
|
||||
# (infra/README.md). With --deploy (D-139) the script does that itself:
|
||||
# 7. copies kvitt-<version_code>.apk, then latest.json, to the Pi with the deploy key infra/secrets/pi_deploy_ed25519,
|
||||
# whose authorized_keys line on the Pi forces `scp -t /home/jacobhagstedt/kvitt/data/apk` (it can only receive
|
||||
# files into that folder; the target path given here is ignored)
|
||||
# 8. checks that https://$KVITT_PUBLIC_HOST/apk/latest returns the new version_code, sending X-Kvitt-App with
|
||||
# kvitt.appKey from android/local.properties (the NPM app gate, D-126; the key is never printed)
|
||||
# Refuses an unsigned build: without the kvitt.release.* keys in android/local.properties the APK cannot be
|
||||
# installed over an earlier release (D-024, D-101).
|
||||
set -euo pipefail
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
# The Kvitt Pi for --deploy (D-139). Constants here, not in infra/pi/.env: .env holds no SSH details (D-125).
|
||||
PI_HOST="gitea.local"
|
||||
PI_USER="jacobhagstedt"
|
||||
PI_SSH_PORT="22"
|
||||
deploy_key="$repo_root/infra/secrets/pi_deploy_ed25519"
|
||||
known_hosts="$repo_root/infra/secrets/pi_known_hosts"
|
||||
local_properties="$repo_root/android/local.properties"
|
||||
|
||||
deploy=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--deploy) deploy=1 ;;
|
||||
-h|--help) echo "Usage: infra/publish-apk.sh [--deploy]"; exit 0 ;;
|
||||
*) echo "publish-apk.sh: unknown argument $arg (usage: infra/publish-apk.sh [--deploy])" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# The app key for the NPM gate (D-126), read, never printed.
|
||||
read_app_key() {
|
||||
[[ -f "$local_properties" ]] || return 0
|
||||
sed -n 's/^[[:space:]]*kvitt\.appKey[[:space:]]*[=:][[:space:]]*//p' "$local_properties" | tail -n 1 | tr -d '\r'
|
||||
}
|
||||
|
||||
if (( deploy )); then
|
||||
[[ -f "$deploy_key" ]] || { echo "publish-apk.sh: --deploy needs the deploy key infra/secrets/pi_deploy_ed25519, which is missing (infra/README.md, Publishing an APK)." >&2; exit 1; }
|
||||
[[ -n "$(read_app_key)" ]] || { echo "publish-apk.sh: --deploy needs kvitt.appKey in android/local.properties to check /apk/latest through the app gate (D-126)." >&2; exit 1; }
|
||||
fi
|
||||
|
||||
[[ -z "$(git status --porcelain)" ]] || { echo "publish-apk.sh: the working tree is dirty; commit or stash first." >&2; exit 1; }
|
||||
|
||||
version_code="$(git rev-list --count HEAD)"
|
||||
@@ -95,6 +129,32 @@ done
|
||||
|
||||
echo "Staged in infra/pi/out/apk/: $file_name ($size_bytes bytes, sha256 $sha256) and latest.json."
|
||||
echo "Built for the release server https://$KVITT_PUBLIC_HOST (KVITT_PUBLIC_HOST in infra/pi/.env)."
|
||||
echo "Copy to the Pi, the APK first and latest.json last (infra/README.md, Publishing an APK):"
|
||||
echo " scp infra/pi/out/apk/$file_name jacobhagstedt@<kvitt-pi-address>:kvitt/data/apk/"
|
||||
echo " scp infra/pi/out/apk/latest.json jacobhagstedt@<kvitt-pi-address>:kvitt/data/apk/"
|
||||
if (( ! deploy )); then
|
||||
echo "Copy to the Pi, the APK first and latest.json last (infra/README.md, Publishing an APK), or rerun with --deploy:"
|
||||
echo " scp infra/pi/out/apk/$file_name jacobhagstedt@<kvitt-pi-address>:kvitt/data/apk/"
|
||||
echo " scp infra/pi/out/apk/latest.json jacobhagstedt@<kvitt-pi-address>:kvitt/data/apk/"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Deploy (D-139). The forced `scp -t` on the Pi writes into kvitt/data/apk/ whatever the target path; -O selects the
|
||||
# legacy scp protocol that a forced `scp -t` speaks (the SFTP default would not). The APK goes first and latest.json
|
||||
# last, because the server reads latest.json on every GET /apk/latest and must never name a file it does not have.
|
||||
pi_copy() {
|
||||
scp -O -i "$deploy_key" -o IdentitiesOnly=yes -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=accept-new -o "UserKnownHostsFile=$known_hosts" \
|
||||
-P "$PI_SSH_PORT" "$1" "$PI_USER@$PI_HOST:"
|
||||
}
|
||||
echo "Copying $file_name to $PI_USER@$PI_HOST ..."
|
||||
pi_copy "$dest/$file_name"
|
||||
echo "Copying latest.json to $PI_USER@$PI_HOST ..."
|
||||
pi_copy "$dest/latest.json"
|
||||
|
||||
# Check what the Pi now serves, through NPM and the app gate; the header is fed on stdin so the key never reaches argv.
|
||||
served="$(printf 'X-Kvitt-App: %s\n' "$(read_app_key)" \
|
||||
| curl -fsS --max-time 30 -H @- "https://$KVITT_PUBLIC_HOST/apk/latest")" \
|
||||
|| { echo "publish-apk.sh: copied, but GET https://$KVITT_PUBLIC_HOST/apk/latest failed." >&2; exit 1; }
|
||||
served_code="$(printf '%s' "$served" | python3 -c 'import json, sys; print(json.load(sys.stdin).get("version_code", ""))')" \
|
||||
|| { echo "publish-apk.sh: copied, but /apk/latest did not return JSON." >&2; exit 1; }
|
||||
[[ "$served_code" == "$version_code" ]] \
|
||||
|| { echo "publish-apk.sh: copied, but the Pi serves version_code $served_code, expected $version_code." >&2; exit 1; }
|
||||
echo "Pi serves version_code $served_code"
|
||||
|
||||
Reference in New Issue
Block a user