feat(admin): install the companion and media apps from the admin script #131

Merged
albert merged 1 commit from feat/admin-installs-android-apps into main 2026-08-17 23:26:33 +00:00
Owner

shepherd-admin apps install companion|media fetches an APK and pushes it
onto an attached Android device over adb. Where the APK comes from follows
how shepherd itself was installed, which is the point: a source checkout
builds the app's own Gradle project, so you install what you just wrote,
while a packaged install downloads the release asset matching its VERSION
from Forgejo and verifies it against the .sha256 sidecar release.yml
uploads beside every asset. --source / --release override the guess;
--version and --apk cover the rest.

It lives in scripts/lib/admin.sh so both entrypoints get it — shepherd
from a checkout, shepherd-admin from the .deb — which is what makes the
source/packaged split expressible in one place.

The details that matter:

  • No root. adb authorises devices against the invoking user's key, so a
    sudo run turns an authorised phone into unauthorized, and a root Gradle
    run leaves root-owned build output in the checkout. The command refuses
    sudo and says why.
  • The device is resolved before anything is built or downloaded — a missing
    phone shouldn't cost a Gradle run — and a HOST:PORT serial is connected
    first, which is how the Fire TV sticks are reached.
  • An unverifiable download is fatal, not a warning: this file is about to
    be installed on a family device. Cached APKs are re-verified before
    reuse, so a truncated download self-heals.
  • INSTALL_FAILED_UPDATE_INCOMPATIBLE (a debug build meeting a release- or
    F-Droid-signed copy) is translated into the adb uninstall it needs —
    with a warning, for the companion app, that this erases the admin records
    and claim tokens for every device that phone administers.
  • Gradle's output goes to stderr: the function's stdout is the APK path,
    and a build log mixed into it becomes the filename.

adb and curl join the .deb as Suggests rather than Depends — a kiosk that
never has a phone plugged into it shouldn't carry the Android platform
tools — and both are checked at call time with the apt line that fixes it.

Verified against the real release server and a phone on USB: the source
build for both apps (including the media app's cargo-ndk cross-compile),
the release download for 0.3.5 and 0.3.6, and a packaged layout simulated
under a fake /usr/lib/shepherd, which downloaded, checksum-verified, and
installed shepherd-media 0.3.6 — release-signed with the certificate
dist/fdroid pins, launching cleanly on the device. Failure paths exercised
too: 404, corrupted cache, unknown serial, sudo, missing adb/curl.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_014RTqj36qW8Vax6GZ86pBAo

`shepherd-admin apps install companion|media` fetches an APK and pushes it onto an attached Android device over adb. Where the APK comes from follows how shepherd itself was installed, which is the point: a source checkout builds the app's own Gradle project, so you install what you just wrote, while a packaged install downloads the release asset matching its VERSION from Forgejo and verifies it against the .sha256 sidecar release.yml uploads beside every asset. `--source` / `--release` override the guess; `--version` and `--apk` cover the rest. It lives in scripts/lib/admin.sh so both entrypoints get it — `shepherd` from a checkout, `shepherd-admin` from the .deb — which is what makes the source/packaged split expressible in one place. The details that matter: * No root. adb authorises devices against the *invoking user's* key, so a sudo run turns an authorised phone into `unauthorized`, and a root Gradle run leaves root-owned build output in the checkout. The command refuses sudo and says why. * The device is resolved before anything is built or downloaded — a missing phone shouldn't cost a Gradle run — and a HOST:PORT serial is connected first, which is how the Fire TV sticks are reached. * An unverifiable download is fatal, not a warning: this file is about to be installed on a family device. Cached APKs are re-verified before reuse, so a truncated download self-heals. * INSTALL_FAILED_UPDATE_INCOMPATIBLE (a debug build meeting a release- or F-Droid-signed copy) is translated into the `adb uninstall` it needs — with a warning, for the companion app, that this erases the admin records and claim tokens for every device that phone administers. * Gradle's output goes to stderr: the function's stdout is the APK path, and a build log mixed into it becomes the filename. adb and curl join the .deb as Suggests rather than Depends — a kiosk that never has a phone plugged into it shouldn't carry the Android platform tools — and both are checked at call time with the apt line that fixes it. Verified against the real release server and a phone on USB: the source build for both apps (including the media app's cargo-ndk cross-compile), the release download for 0.3.5 and 0.3.6, and a packaged layout simulated under a fake /usr/lib/shepherd, which downloaded, checksum-verified, and installed shepherd-media 0.3.6 — release-signed with the certificate dist/fdroid pins, launching cleanly on the device. Failure paths exercised too: 404, corrupted cache, unknown serial, sudo, missing adb/curl. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RTqj36qW8Vax6GZ86pBAo
feat(admin): install the companion and media apps from the admin script
All checks were successful
CI / Version harmony (pull_request) Successful in 13s
CI / Arch neutrality (pull_request) Successful in 13s
CI / ShellCheck (pull_request) Successful in 17s
CI / CI image (pull_request) Successful in 23s
CI / CI image (Android) (pull_request) Successful in 22s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 25s
CI / Warm cargo registry (pull_request) Successful in 35s
CI / Android media (cargo-ndk build) (pull_request) Successful in 44s
CI / Android companion (unit tests) (pull_request) Successful in 1m59s
CI / Build (pull_request) Successful in 2m44s
CI / Clippy (pull_request) Successful in 1m22s
CI / Test (pull_request) Successful in 2m50s
CI / E2E (pull_request) Successful in 2m53s
CI / Firewall E2E (pull_request) Successful in 3m51s
CI / Package (.deb smoke build) (pull_request) Successful in 2m5s
6c0a3422bb
`shepherd-admin apps install companion|media` fetches an APK and pushes it
onto an attached Android device over adb. Where the APK comes from follows
how shepherd itself was installed, which is the point: a source checkout
builds the app's own Gradle project, so you install what you just wrote,
while a packaged install downloads the release asset matching its VERSION
from Forgejo and verifies it against the .sha256 sidecar release.yml
uploads beside every asset. `--source` / `--release` override the guess;
`--version` and `--apk` cover the rest.

It lives in scripts/lib/admin.sh so both entrypoints get it — `shepherd`
from a checkout, `shepherd-admin` from the .deb — which is what makes the
source/packaged split expressible in one place.

The details that matter:

* No root. adb authorises devices against the *invoking user's* key, so a
  sudo run turns an authorised phone into `unauthorized`, and a root Gradle
  run leaves root-owned build output in the checkout. The command refuses
  sudo and says why.
* The device is resolved before anything is built or downloaded — a missing
  phone shouldn't cost a Gradle run — and a HOST:PORT serial is connected
  first, which is how the Fire TV sticks are reached.
* An unverifiable download is fatal, not a warning: this file is about to
  be installed on a family device. Cached APKs are re-verified before
  reuse, so a truncated download self-heals.
* INSTALL_FAILED_UPDATE_INCOMPATIBLE (a debug build meeting a release- or
  F-Droid-signed copy) is translated into the `adb uninstall` it needs —
  with a warning, for the companion app, that this erases the admin records
  and claim tokens for every device that phone administers.
* Gradle's output goes to stderr: the function's stdout is the APK path,
  and a build log mixed into it becomes the filename.

adb and curl join the .deb as Suggests rather than Depends — a kiosk that
never has a phone plugged into it shouldn't carry the Android platform
tools — and both are checked at call time with the apt line that fixes it.

Verified against the real release server and a phone on USB: the source
build for both apps (including the media app's cargo-ndk cross-compile),
the release download for 0.3.5 and 0.3.6, and a packaged layout simulated
under a fake /usr/lib/shepherd, which downloaded, checksum-verified, and
installed shepherd-media 0.3.6 — release-signed with the certificate
dist/fdroid pins, launching cleanly on the device. Failure paths exercised
too: 404, corrupted cache, unknown serial, sudo, missing adb/curl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RTqj36qW8Vax6GZ86pBAo
albert merged commit 089262181f into main 2026-08-17 23:26:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
albert/shepherd-launcher!131
No description provided.