Files
musicshare/docs/ANDROID-CI.md
T
chrisandClaude Opus 5.5 3b39910f13 Build the Android app on our own Gitea runner
Pushing a v* tag builds the normal and FOSS APKs, signed with the upload
key, and attaches them to a Gitea release; a manual run on a branch
uploads a test build to the repo's packages. Everything runs on the
Gitea server: a builder image with the Android SDK, an act_runner
registered only for this repo, and plain shell steps (no third-party
actions). The build itself is apps/mobile/scripts/build-android.sh, the
same script for hand builds. Setup steps are in docs/ANDROID-CI.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:28:52 -04:00

3.5 KiB

Building the Android app on the Gitea server

Pushing a version tag builds both APKs on our own server and attaches them to a Gitea release:

  • musicshare-<version>.apk: the normal app.
  • musicshare-<version>-foss.apk: no closed-source code, the same build F-Droid makes. The build fails if any Google ML Kit or Play Services code ends up inside.

Both are signed with the upload key, and SHA256SUMS lists their checksums. Nothing leaves the server except the downloads the build itself needs (npm packages, Gradle and its plugins).

The pieces:

File What it is
.gitea/workflows/android.yml The workflow. It uses only plain shell steps, no third-party actions.
apps/mobile/scripts/build-android.sh The build. You can also run it by hand on any machine with the Android SDK.
deploy/android-builder/Dockerfile The build environment: Node 22, Java 17 and the Android SDK/NDK.
deploy/gitea-runner/ The runner (act_runner) that runs builds on the server.

One-time setup (on the Gitea server)

The server needs about 8 GB of free memory during a build and about 15 GB of disk for the image and caches. A first build takes 30-40 minutes; later ones reuse the Gradle cache.

  1. Turn on Actions if it's off. In Gitea's app.ini, set:

    [actions]
    ENABLED = true
    

    Restart Gitea. Then, in the repo, go to Settings > Repository > Actions and enable it.

  2. Build the builder image from a checkout of this repo:

    docker build -t musicshare-android-builder deploy/android-builder
    

    Rebuild it when React Native's SDK or NDK versions change (the Dockerfile says where to look).

  3. Start the runner. In the repo, open Settings > Actions > Runners > Create new runner and copy the token. Use the repo's page, not the site admin's, so the runner only takes this repo's jobs. Then:

    cd deploy/gitea-runner
    RUNNER_TOKEN=<token> docker compose up -d
    

    It registers once (saved in ./data), then shows as idle with the label musicshare-android. The runner controls Docker on the host, so treat it like root access. The workflow only runs for tags and manual runs, never for pull requests.

  4. Add the secrets under Settings > Actions > Secrets:

    • UPLOAD_KEYSTORE_BASE64: the output of base64 -w0 ~/.config/musicshare/musicshare-upload.jks.
    • UPLOAD_STORE_PASSWORD, UPLOAD_KEY_ALIAS (musicshare-upload), UPLOAD_KEY_PASSWORD: the values in ~/.gradle/gradle.properties on the machine you've been building on.
    • RELEASE_TOKEN: create it under User settings > Applications > Access tokens, with write:repository and write:package permissions.

Releasing

  1. Bump version and android.versionCode in apps/mobile/app.json. Add fastlane/metadata/android/en-US/changelogs/<versionCode>.txt, then commit.
  2. Tag and push: git tag v1.1.0 && git push origin main v1.1.0.
  3. Watch it under the repo's Actions tab. The APKs appear on the Releases page.

The build refuses to run if the tag doesn't match the version in app.json. Re-running a tag's build replaces that release's files.

Test builds: go to Actions > Android > Run workflow and pick a branch. The APKs go to the repo's Packages as musicshare-android, versioned <branch>-<commit>, instead of to a release.

Building by hand

apps/mobile/scripts/build-android.sh            # APKs in apps/mobile/dist

It uses the upload key from ~/.gradle/gradle.properties if it's there; otherwise the APKs come out unsigned.