Add CI-built binary releases (.deb + .apk) (#82) #90

Merged
albert merged 16 commits from u/albert/82/binary-releases into main 2026-07-11 05:00:47 +00:00
Owner

Introduce a tag-driven release pipeline that publishes an amd64 .deb and a
signed per-module .apk to Forgejo releases.

  • scripts/lib/package.sh: shepherd package deb stages the install tree by
    driving install.sh with DESTDIR (single-sourcing the layout) and wraps it
    with dpkg-deb; runtime Depends derive from run.pkgs, and conffiles /
    postinst reuse install.sh's own path/group constants. Re-execs under
    fakeroot when unprivileged.
  • companion-android: derive versionCode from VERSION and add a guarded
    release signingConfig fed from CI env (falls back to debug locally).
  • .github/workflows/release.yml: guard (tag == VERSION) -> build .deb + a
    matrix of signed .apks -> publish to Forgejo via the API with SHA256SUMS.
    The apk job is structured to fan out over multiple Android modules.
  • .github/workflows/images.yml: extract the build-if-missing CI image jobs
    into a reusable workflow shared by ci.yml and release.yml.
  • ci.yml: call images.yml; add a package job that smoke-builds the .deb on
    every push/PR (no publish) to catch packaging breakage before a tag.
  • docs/INSTALL.md: document installing from the .deb and the post-install
    per-user steps the package intentionally leaves to the admin.

Requires new secrets before the first release: FORGEJO_TOKEN and the
SHEPHERD_KEYSTORE_* signing set. Design doc:
docs/ai/history/2026-07-04 003 binary-releases.md

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

Fixes #82

Introduce a tag-driven release pipeline that publishes an amd64 .deb and a signed per-module .apk to Forgejo releases. - scripts/lib/package.sh: `shepherd package deb` stages the install tree by driving install.sh with DESTDIR (single-sourcing the layout) and wraps it with dpkg-deb; runtime Depends derive from run.pkgs, and conffiles / postinst reuse install.sh's own path/group constants. Re-execs under fakeroot when unprivileged. - companion-android: derive versionCode from VERSION and add a guarded release signingConfig fed from CI env (falls back to debug locally). - .github/workflows/release.yml: guard (tag == VERSION) -> build .deb + a matrix of signed .apks -> publish to Forgejo via the API with SHA256SUMS. The apk job is structured to fan out over multiple Android modules. - .github/workflows/images.yml: extract the build-if-missing CI image jobs into a reusable workflow shared by ci.yml and release.yml. - ci.yml: call images.yml; add a `package` job that smoke-builds the .deb on every push/PR (no publish) to catch packaging breakage before a tag. - docs/INSTALL.md: document installing from the .deb and the post-install per-user steps the package intentionally leaves to the admin. Requires new secrets before the first release: FORGEJO_TOKEN and the SHEPHERD_KEYSTORE_* signing set. Design doc: docs/ai/history/2026-07-04 003 binary-releases.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86 Fixes #82
Add CI-built binary releases (.deb + .apk) (#82)
Some checks failed
CI / Version harmony (pull_request) Successful in 8s
CI / ShellCheck (pull_request) Successful in 10s
CI / CI image (pull_request) Failing after 19s
CI / CI images (pull_request) Failing after 0s
CI / Build (pull_request) Has been skipped
CI / Test (pull_request) Has been skipped
CI / E2E (pull_request) Has been skipped
CI / Clippy (pull_request) Has been skipped
CI / Rustfmt (pull_request) Has been skipped
CI / Package (.deb smoke build) (pull_request) Has been skipped
CI / Firewall E2E (pull_request) Has been skipped
CI / Android portability (shepherd-media-core) (pull_request) Has been skipped
CI / Android companion (unit tests) (pull_request) Has been skipped
CI / CI image (Android) (pull_request) Has been skipped
654b24b73f
Introduce a tag-driven release pipeline that publishes an amd64 .deb and a
signed per-module .apk to Forgejo releases.

- scripts/lib/package.sh: `shepherd package deb` stages the install tree by
  driving install.sh with DESTDIR (single-sourcing the layout) and wraps it
  with dpkg-deb; runtime Depends derive from run.pkgs, and conffiles /
  postinst reuse install.sh's own path/group constants. Re-execs under
  fakeroot when unprivileged.
- companion-android: derive versionCode from VERSION and add a guarded
  release signingConfig fed from CI env (falls back to debug locally).
- .github/workflows/release.yml: guard (tag == VERSION) -> build .deb + a
  matrix of signed .apks -> publish to Forgejo via the API with SHA256SUMS.
  The apk job is structured to fan out over multiple Android modules.
- .github/workflows/images.yml: extract the build-if-missing CI image jobs
  into a reusable workflow shared by ci.yml and release.yml.
- ci.yml: call images.yml; add a `package` job that smoke-builds the .deb on
  every push/PR (no publish) to catch packaging breakage before a tag.
- docs/INSTALL.md: document installing from the .deb and the post-install
  per-user steps the package intentionally leaves to the admin.

Requires new secrets before the first release: FORGEJO_TOKEN and the
SHEPHERD_KEYSTORE_* signing set. Design doc:
docs/ai/history/2026-07-04 003 binary-releases.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
Follow-up to the release pipeline, removing duplication surfaced by an audit
of scripts/lib/package.sh vs the from-source install path:

- install.sh: extract install_system() as the single source of truth for the
  system-wide install components. install_all now calls it (then adds the
  per-user config/groups steps), and package_deb stages exactly it under
  DESTDIR — so a new system component is added in one place, not two.
- package.sh: ship config.example.toml + movies-library.example.toml to
  /usr/share/shepherd/. A .deb user has no repo, so the previous INSTALL.md
  steps (`shepherd install config/groups`) were impossible — the shepherd CLI
  isn't packaged and hard-requires a source checkout. The generated postinst
  now prints the exact per-user copy/usermod commands, with the group set
  injected from install.sh's SHEPHERD_REQUIRED_GROUPS + FIREWALL_GROUP so it
  can't drift.
- INSTALL.md: replace the CLI-based .deb steps with the packaged-example
  commands.
- Cross-reference comments tie the postinst's host-mutation back to
  install_firewall / install_udev (inherent bash<->sh duplication that can
  only be kept in sync, since the maintainer script runs without the repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
ci: fix empty CI_IMAGE_NAME in the reusable images workflow (#82)
Some checks failed
CI / Version harmony (pull_request) Successful in 5s
CI / ShellCheck (pull_request) Successful in 8s
CI / CI image (pull_request) Failing after 21s
CI / CI images (pull_request) Failing after 0s
CI / Build (pull_request) Has been skipped
CI / Test (pull_request) Has been skipped
CI / E2E (pull_request) Has been skipped
CI / Clippy (pull_request) Has been skipped
CI / Rustfmt (pull_request) Has been skipped
CI / Package (.deb smoke build) (pull_request) Has been skipped
CI / Firewall E2E (pull_request) Has been skipped
CI / Android portability (shepherd-media-core) (pull_request) Has been skipped
CI / Android companion (unit tests) (pull_request) Has been skipped
CI / CI image (Android) (pull_request) Has been skipped
18e0f80751
Forgejo's runner does not propagate a reusable (workflow_call) workflow's
top-level `env:` into its jobs, so CI_IMAGE_NAME was blank and the image ref
computed to an invalid `:2026wNN-<hash>` (tag with no registry/name), failing
`docker build -t`. Move CI_IMAGE_NAME to job-level env on both image jobs,
which the runner does honor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
Merge remote-tracking branch 'origin/main' into u/albert/82/binary-releases
All checks were successful
CI / Version harmony (pull_request) Successful in 6s
CI / ShellCheck (pull_request) Successful in 8s
CI / CI image (pull_request) Successful in 18s
CI / CI image (Android) (pull_request) Successful in 18s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 52s
CI / Test (pull_request) Successful in 6m57s
CI / Build (pull_request) Successful in 7m35s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 37s
CI / E2E (pull_request) Successful in 8m40s
CI / Clippy (pull_request) Successful in 8m44s
CI / Android companion (unit tests) (pull_request) Successful in 1m43s
CI / Firewall E2E (pull_request) Successful in 3m14s
CI / Package (.deb smoke build) (pull_request) Successful in 12m43s
faff24cd09
ci: reconcile media-android with the release pipeline (#82, #72)
All checks were successful
CI / Version harmony (pull_request) Successful in 24s
CI / ShellCheck (pull_request) Successful in 28s
CI / CI image (pull_request) Successful in 33s
CI / CI image (Android) (pull_request) Successful in 30s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 56s
CI / Clippy (pull_request) Successful in 3m24s
CI / Test (pull_request) Successful in 3m52s
CI / Build (pull_request) Successful in 4m5s
CI / E2E (pull_request) Successful in 4m25s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 56s
CI / Android companion (unit tests) (pull_request) Successful in 1m23s
CI / Android media (cargo-ndk build) (pull_request) Successful in 1m0s
CI / Package (.deb smoke build) (pull_request) Successful in 5m3s
CI / Firewall E2E (pull_request) Successful in 3m43s
4dcd1c2c0e
Merging main brought in the shepherd-media Android app (#72), which added an
`android-media` job to ci.yml. Because this branch had refactored the image
jobs into the reusable images.yml workflow, git merged main's job in verbatim
and it still referenced the now-removed `image-android` job. Fix it to use the
`images` caller and its `android-ref` output.

Then wire media in as the second release APK:

- release.yml: add a `media` row to the apk matrix. Unlike the pure-Kotlin
  companion, its Gradle assembleRelease cross-compiles a Rust cdylib via
  cargo-ndk for both packaged ABIs (arm64-v8a + armeabi-v7a), so the row carries
  `rust: true` gating cargo-ndk/target/NDK setup + a cargo cache (key shared
  with ci.yml's android-media job). The vendored libmpv .so's for both ABIs are
  already in-repo; the 32-bit bindgen path uses the base image's libclang.
- crates/shepherd-media-android/android/app/build.gradle.kts: derive
  versionCode/versionName from the repo-root VERSION and add the guarded release
  signingConfig, matching companion-android (previously hardcoded 0.1.0/1 and
  unsigned, which can't ship as an upgradable release).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
yt-dlp is not apt-installed — the from-source path builds it into a venv at
/opt/shepherd/ytdlp-venv via `shepherd deps install run` (install_ytdlp), which
a .deb user never runs, so YouTube media libraries silently break. The .deb only
Depends on python3-venv (the toolchain), not yt-dlp itself.

Print the venv+pip+symlink setup in the generated postinst guidance (paths
injected from deps.sh's YTDLP_VENV/YTDLP_LINK so they can't drift from
install_ytdlp), and document it in INSTALL.md. Only YouTube libraries need it;
local mpv playback is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
scripts: share post-install admin tasks via a shepherd-admin CLI (#82)
Some checks failed
CI / Version harmony (pull_request) Successful in 23s
CI / ShellCheck (pull_request) Successful in 27s
CI / CI image (pull_request) Successful in 11m22s
CI / CI image (Android) (pull_request) Successful in 7m14s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 57s
CI / Clippy (pull_request) Failing after 5m15s
CI / Build (pull_request) Successful in 8m28s
CI / Test (pull_request) Successful in 9m3s
CI / E2E (pull_request) Successful in 9m21s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 58s
CI / Android companion (unit tests) (pull_request) Successful in 55s
CI / Firewall E2E (pull_request) Successful in 4m50s
CI / Android media (cargo-ndk build) (pull_request) Successful in 2m14s
CI / Package (.deb smoke build) (pull_request) Successful in 11m4s
69496f217a
The admin tasks useful on an installed system (yt-dlp, bluetooth clear,
hardening, and now Flathub app installs + per-user setup) lived only in the
repo-bound `scripts/shepherd`, so a .deb user — with no source tree — couldn't
run them; the postinst could only print raw commands. Refactor them into a
shared library backing two entrypoints.

- lib/admin.sh (new): repo-independent admin tasks — yt-dlp (moved out of
  deps.sh), `apps install steam|chrome` (Flathub), and `setup-user` (config +
  all group memberships). harden.sh/bluetooth.sh were already repo-independent.
- lib/common.sh: get_data_dir() — repo root in source, /usr/share/shepherd when
  packaged (SHEPHERD_DATA_DIR). install_config reads examples via it.
- lib/install.sh: extract add_user_to_groups(), shared by install_user_groups
  and setup_user (which also adds shepherd-firewall).
- scripts/shepherd-admin (new): slim entrypoint that resolves its libs from a
  sibling lib/ dir and self-detects source-vs-packaged layout. Omits
  build/package/dev.
- scripts/shepherd: sources admin.sh; gains `apps` / `setup-user` for parity.
- package.sh: ships lib/*.sh + shepherd-admin under /usr/lib/shepherd
  (symlinked to /usr/bin/shepherd-admin) + VERSION; postinst guidance collapses
  to `shepherd-admin setup-user USER` / `yt-dlp install`.
- INSTALL.md, scripts/README.md, ci.yml shellcheck updated.

Verified: shellcheck clean; shepherd-admin works from the source tree and from
the extracted .deb (libs source from the packaged layout, data dir resolves to
/usr/share/shepherd); the .deb ships the CLI + libs + VERSION + symlink.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
shepherd's type="steam" adapter drives Canonical's Steam *snap*
(config.example.toml: `snap install steam`), not the flatpak. `apps install
steam` was installing com.valvesoftware.Steam from Flathub, which the
integration doesn't use. Route steam through `snap install steam` and keep
chrome on the Flathub flatpak (com.google.Chrome), which kind="flatpak" does
wrap. Fix the "from Flathub" wording in the CLIs, INSTALL.md, and the doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
release.yml only triggers on v* tags, so its first run would be a production
release with untested apk signing, the media armeabi-v7a cross-compile, and the
Forgejo publish path. Add a manual trigger with a `publish` input (default off):
it builds the .deb + both signed .apks and runs collect/checksum — exercising
signing and the artifacts round-trip — but skips creating the release unless
`publish` is ticked. The guard's tag==VERSION check is skipped for dispatch;
create-release is gated on push-or-publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
Add `shepherd-admin power-key <suspend|default|status>` (also `shepherd
power-key` from source), which writes a systemd-logind drop-in
(/etc/systemd/logind.conf.d/10-shepherd-power-key.conf) setting
HandlePowerKey=suspend so a short press of the hardware power button sleeps the
device instead of powering off. A long press still powers off. logind is
restarted so it takes effect; `default` removes the override; `status` reports
it. shepherdd already draws the suspend cover on logind's PrepareForSleep, so
the device wakes back into the session cleanly. Shipped in the .deb via admin.sh
and documented in INSTALL.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
ci(release): rename FORGEJO_TOKEN secret to RELEASE_TOKEN (#82)
Some checks failed
CI / Version harmony (pull_request) Successful in 24s
CI / ShellCheck (pull_request) Successful in 27s
CI / CI image (pull_request) Successful in 31s
CI / CI image (Android) (pull_request) Successful in 29s
CI / CI images (pull_request) Successful in 0s
CI / Android portability (shepherd-media-core) (pull_request) Waiting to run
CI / Android companion (unit tests) (pull_request) Waiting to run
CI / Android media (cargo-ndk build) (pull_request) Waiting to run
CI / Rustfmt (pull_request) Successful in 1m2s
CI / Clippy (pull_request) Failing after 5m1s
CI / Firewall E2E (pull_request) Has been cancelled
CI / Package (.deb smoke build) (pull_request) Has been cancelled
CI / E2E (pull_request) Has been cancelled
CI / Test (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
af38c23986
Forgejo rejects secret names starting with FORGEJO_/GITEA_/GITHUB_, so
FORGEJO_TOKEN can't be registered. Rename to RELEASE_TOKEN (matching the
existing REGISTRY_TOKEN style) in the publish job and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
fix(media): suffix f32 stroke-width literals for the newer toolchain
Some checks failed
CI / Version harmony (pull_request) Successful in 15s
CI / ShellCheck (pull_request) Successful in 31s
CI / CI image (pull_request) Successful in 42s
CI / CI image (Android) (pull_request) Successful in 41s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 56s
CI / Clippy (pull_request) Failing after 5m54s
CI / Build (pull_request) Successful in 8m2s
CI / Test (pull_request) Successful in 8m54s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 58s
CI / E2E (pull_request) Successful in 9m17s
CI / Android companion (unit tests) (pull_request) Successful in 1m27s
CI / Firewall E2E (pull_request) Successful in 4m36s
CI / Android media (cargo-ndk build) (pull_request) Successful in 2m38s
CI / Package (.deb smoke build) (pull_request) Successful in 11m13s
9bd495694c
A weekly CI-image rebuild pulled a Rust toolchain that turns
float_literal_f32_fallback into a hard error under -D warnings. egui's
Stroke::new takes `width: impl Into<f32>`, so a bare `4.0` literal defaults to
f64 and falls back to f32 (f64: Into<f32> is unsatisfied). Suffix the three
Stroke::new width literals (grid.rs, media-android ui.rs) as clippy suggests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
fix(host-linux): drop redundant pid: _ before .. in match pattern
All checks were successful
CI / Version harmony (pull_request) Successful in 22s
CI / ShellCheck (pull_request) Successful in 26s
CI / CI image (pull_request) Successful in 31s
CI / CI image (Android) (pull_request) Successful in 30s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 58s
CI / Clippy (pull_request) Successful in 5m44s
CI / Build (pull_request) Successful in 7m40s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 1m0s
CI / E2E (pull_request) Successful in 9m0s
CI / Test (pull_request) Successful in 9m10s
CI / Android companion (unit tests) (pull_request) Successful in 1m19s
CI / Firewall E2E (pull_request) Successful in 4m37s
CI / Android media (cargo-ndk build) (pull_request) Successful in 2m23s
CI / Package (.deb smoke build) (pull_request) Successful in 11m7s
1fe0466c61
Rust 1.97's clippy flags `unneeded_wildcard_pattern`: in
`HostHandlePayload::Linux { pid: _, .. }` the `..` already covers `pid`, so the
explicit `pid: _` is redundant under -D warnings. Drop it.

Verified with the full `cargo clippy --all-targets --workspace -- -D warnings`
sweep on 1.97.0 (the toolchain the rebuilt CI image now carries): with this and
the f32-literal fix, the workspace clippy pass is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
feat(admin): enable user namespaces when installing Steam (#82)
All checks were successful
CI / Version harmony (pull_request) Successful in 21s
CI / ShellCheck (pull_request) Successful in 24s
CI / CI image (pull_request) Successful in 26s
CI / CI image (Android) (pull_request) Successful in 30s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 58s
CI / Clippy (pull_request) Successful in 5m55s
CI / Build (pull_request) Successful in 7m9s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 1m3s
CI / Test (pull_request) Successful in 9m3s
CI / E2E (pull_request) Successful in 9m14s
CI / Android companion (unit tests) (pull_request) Successful in 1m19s
CI / Firewall E2E (pull_request) Successful in 4m30s
CI / Android media (cargo-ndk build) (pull_request) Successful in 2m29s
CI / Package (.deb smoke build) (pull_request) Successful in 11m5s
8e39329f93
The Steam snap creates an unprivileged user namespace for its sandbox, but
Ubuntu 23.10+ restricts those by default
(kernel.apparmor_restrict_unprivileged_userns=1), so on a fresh system Steam
fails with "Steam now requires user namespaces to be enabled". (A machine that
already ran a Flatpak app usually has userns working, which is why Steam appears
fine there — the reported symptom.)

`apps install steam` now connects the snap's mount-observe interface (the
steam-snap maintainers' fix) and permits unprivileged userns via a
/etc/sysctl.d/90-shepherd-userns.conf drop-in (applied live too). Guarded: no-op
when the kernel lacks the knob or it's already permitted; warns that it relaxes
the hardening system-wide and how to revert. Documented in INSTALL.md.

Ref: https://github.com/canonical/steam-snap/issues/519

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNM2TbcjK51PR3ecyuyD86
Author
Owner

code looks reasonable, but testing this properly relies it being in main...

code looks reasonable, but testing this properly relies it being in main...
albert merged commit 3a99809613 into main 2026-07-11 05:00:47 +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!90
No description provided.