Upstream wrapper-v2 ships source only, so Waves publishes its own pinned
image (ghcr.io/ranokay/waves-wrapper-v2:0.2.4, digest
sha256:79a36375a3555ca9e4aa6a9d1ccffbf0ac45a1604d19d307761c6d6ba29b428b)
for end-user pulls. This page covers who uses what, then the maintainer
runbook: one-time setup, how to publish, and the version lockstep.
- You just use Waves. Nothing to do, nothing to host. The app pulls
the public image on demand, runs it while downloading, stops it when
idle. Apple ID sign-in happens inside the guest at first use, from the
setup wizard's login step; you never supply the APK yourself — the image
carries the guest libraries. Trust note, stated plainly: the image
contains Apple's native
.sofiles (that is what makes ALAC decryption possible). It was built from the pinned upstream source plus the blessed APK below; the publish summary in Actions records the exact source SHA and guest-lib pins, so anyone can audit what went in. Images published after 2026-09-15 also ship their third-party notices under/licensesand OCI provenance labels (the current0.2.4carries them; the earlier0.2.3predates them), and the app verifies the pinned digest when it pulls; the distribution decision and its accepted risk are recorded inwrapper-image-license-review.md. - You fork or clone Waves to hack on it. Still nothing to do: the app pin points at the public image, which pulls anonymously. Develop, run, test Apple downloads — no secrets, no builds.
- You maintain a fork (or upstream) with your own image. Run the same
wrapper-imageworkflow in your repo: it publishes toghcr.io/<your-name>/waves-wrapper-v2automatically (set your ownAPK_URL/APK_AUTH_HEADERsecrets there the same way), flip your package public, then move this repo'sWRAPPER_V2_IMAGEpin (waves/providers/apple/runtime.py) to your path. That one-line pin edit is the only code change a private image ever needs. Version lockstep below applies to you exactly as written. - You send Apple work upstream as a PR. The workflow is
workflow_dispatch-only: it never runs on PRs, needs no secrets from contributors, and never bundles binaries — so a fork PR stays green and legal. Never commit an.apk/.apkm(git-ignored) or paste tokens. Upstream publishes from its own secrets when it ships Apple work, the same way this fork does. - Reproducibility. Same inputs (upstream SHA + APK bytes) yield the same staged libs (hash-verified at build); image bytes may still differ (timestamps), so provenance is the Actions run summary, not digest equality.
| Secret | Where (repo settings) | Used by | Who needs it |
|---|---|---|---|
APK_URL |
image-publishing repo | wrapper-image job | maintainer only |
APK_AUTH_HEADER |
image-publishing repo | wrapper-image job | maintainer only |
| Apple ID + 2FA | never stored anywhere | local guest login | whoever runs the downloads |
- Host the APK privately. Recommended: a private repo release
asset. Create a private repo (e.g.
ranokay/waves-assets), upload the pinned version's.apkm(currently 3.6.0-beta, build 1109, arm64 — the setup wizard names the current pin) as a release asset, and note its.../releases/download/<tag>/<file>URL. The built-inGITHUB_TOKENcannot cross into another private repo, so this needs its own credential:- Create a fine-grained personal access token with Contents: read-only on just that repo (Settings → Developer settings → Personal access tokens → Fine-grained tokens). Note its expiry (max one year) — set a calendar reminder; an expired token fails the build at the download step with a 401.
- Add two repository secrets on this repo (Settings → Secrets and
variables → Actions):
APK_URL= the release asset URL, andAPK_AUTH_HEADER= the full header lineAuthorization: Bearer <token>. The header is sent only to fetch the artifact and is masked in logs. - Alternative without GitHub: a token-gated private bucket (R2/B2)
with its token in
APK_AUTH_HEADER; same shape, another account. - Not recommended: committing the APK anywhere, public hosting, or scraping app-mirror sites at build time (fragile and against their terms).
- Nothing else. Registry auth uses the built-in
GITHUB_TOKEN; Docker, QEMU, and Buildx come with the runner.
Actions → wrapper-image → Run workflow:
wrapper_ref— upstreamwrapper-v2full commit SHA to build. Required, and the run refuses anything that is not 40 hex characters: a branch or tag moves, so it cannot be recorded as provenance.image_tag— the tag to push. It must equal theWRAPPER_V2_IMAGEpin inwaves/providers/apple/runtime.py(the suite enforces this), and it must be new: the run refuses to overwrite an existing tag.allow_tag_overwriteexists only to repair a bad publish and is never set for a release.
The run downloads the APK, extracts and hash-verifies the arm64 native
libs against the source tree's LIBS_VERSION.json, builds linux/arm64,
smoke-tests /health plus the TCP decrypt port under QEMU, pushes the
tag, and prints a summary (image, source SHA, guest libs).
A fresh ghcr.io package is private, and anonymous docker pull fails
against private packages with denied — which is exactly the error
end users would see. Open the package page
(github.com/users/ranokay/packages/container/package/waves-wrapper-v2),
Package settings → Change visibility → Public.
One honest caveat (spec §10.2): the image bakes Apple's .so files, so
a public image redistributes Apple binaries. The trade-off was reviewed and
accepted on 2026-09-15 (wrapper-image-license-review.md), with the notice,
provenance-label and digest fixes that go with it; the alternatives are a
private image (kills one-click setup — every user would need docker login
plus a grant) or a future redesign where user-supplied libs mount at runtime
instead of baking in.
These five move together; bump them as one change:
| Piece | Where | Current |
|---|---|---|
| Image tag | workflow image_tag input |
0.2.4 |
| Image pin | WRAPPER_V2_IMAGE, waves/providers/apple/runtime.py |
…:0.2.4 |
| Image digest | WRAPPER_V2_IMAGE_DIGEST, same file + the digest named at the top of this page |
sha256:79a3…428b |
| APK version | APK_PINNED_VERSION, same file + APK_URL content |
3.6.0-beta (build 1109) |
| Guest libs | regenerated from the blessed APK at build time: upstream's pin file matched 3.6.0-1109 when last checked, but nothing guarantees it tracks the blessed APK, so CI pins deterministically from the file itself; WRAPPER_LIBS_VERSION (17.0.0) is still recorded in the image manifest |
18 arm64 libs |
The digest pin is what a pull reports (docker pull then
docker image inspect --format '{{index .RepoDigests 0}}' <image>); the
workflow's publish summary prints the same value with docker buildx imagetools inspect, so a republish updates the pin from a copy-paste.
When upstream wrapper-v2 fixes something you need (or Apple breaks
something it must adapt to): pick the upstream SHA, rebuild with the same
APK, and — if anything about the image changed — push a new tag and
move the WRAPPER_V2_IMAGE pin to it in the same release. Never retag a
published tag in place: machines that already pulled it would keep the
old bytes.
A weekly scheduled workflow (wrapper-upstream-check, Mondays) compares
upstream main against the tracked pin (.github/wrapper-upstream.sha,
currently the 0.2.4 build SHA) and opens one deduped issue when it moves,
with the compare link and the checklist above. It never builds or publishes
on its own — shipping new decryption code stays a human decision. The same
watch runs on forks, against the same upstream, which is what they want.
After publishing from a new ref, move the pin file forward in that change.
deniedon pull (end users): the package flipped back to private, or the tag was never pushed. Check visibility, then the Actions run.APK_URL secret is not set: step 1 above was skipped on this repo.- Hash mismatch in
extract-libs.sh: theAPK_URLcontent is the wrong APK version or architecture. Re-point it at the pinned version's arm64 release and re-run. - Smoke test flakes under QEMU: emulation is slow; the health loop already waits two minutes. Re-run the job before suspecting the image.