Copy-ready CI examples and reusable actions for publishing app builds to Hands.
-
Install Hands for your Raft server by following the Getting Started guide.
-
Create an app in Hands.
-
Create an app-scoped
publisherdeploy token in App Settings, or with an authorized CLI session:hands deploy-tokens create <app-slug> --name github-actions --role publisher
-
Store the one-time token in your CI secret store as
HANDS_BEARER_TOKEN. -
Build and sign your app in your existing CI, then use one of the examples below to publish the artifact to Hands.
Hands is the release and distribution layer. Your own build system remains responsible for compiling and signing the APK, IPA, or other artifact.
If an APK contains the Hands Android SDK's libhandscrash.so, its exact
native-symbols classifier is a required release artifact. The AAR and APK
contain a stripped library; repacking that file after the build does not
restore function or source information.
Add a resolvable classifier configuration beside the dependency in your Android app module. This GitHub Packages example deliberately uses one version variable for both artifacts:
val handsAndroidSdkVersion = "0.11.2" // keep this as your single source of truth
val handsNativeSymbols by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
isTransitive = false
}
val copyHandsNativeSymbols by tasks.registering(Sync::class) {
from(handsNativeSymbols)
into(layout.buildDirectory.dir("outputs/hands-native-symbols"))
}
dependencies {
implementation("build.hands:hands-android-sdk:$handsAndroidSdkVersion")
add(
handsNativeSymbols.name,
"build.hands:hands-android-sdk:$handsAndroidSdkVersion:native-symbols@zip",
)
}For JitPack, keep the AAR and classifier on JitPack rather than mixing channels:
implementation("com.github.botiverse:hands:android-sdk-v$handsAndroidSdkVersion")
add(
handsNativeSymbols.name,
"com.github.botiverse:hands:android-sdk-v$handsAndroidSdkVersion:native-symbols@zip",
)Run assembleRelease and copyHandsNativeSymbols in the same build. The
validator then enforces all of the following before Hands receives anything:
- every APK ABI containing
libhandscrash.soexists in the archive; - APK, archive, and
manifest.jsonELF build IDs match per ABI; - manifest SHA-256 values match the unstripped files;
- each symbol file contains
.debug_info.
The reusable action defaults to native-symbols-policy: auto: an APK containing
libhandscrash.so fails when symbols is missing. APKs without the Hands native
crash library do not need this archive. disabled is an explicit opt-out that
leaves future native crashes unsymbolicatable; do not use it to make a release
green.
For split or external build jobs, transport the APK and classifier archive as one immutable artifact set. Never download an APK and generate “symbols” from its stripped libraries later.
The composite action at publish-android/action.yml
uploads an already-built, signed APK and creates a draft release:
- name: Publish draft to Hands
uses: botiverse/hands-examples/publish-android@v1
with:
app: my-android-app
apk: app/build/outputs/apk/release/app-release.apk
channel: preview
version-name: ${{ github.ref_name }}
version-code: ${{ github.run_number }}
changelog-file: changelog.txt
symbols: path/to/the-resolved/hands-native-symbols.zip
native-symbols-policy: required
hands-token: ${{ secrets.HANDS_BEARER_TOKEN }}The action defaults to draft: true. Activating a release remains a separate,
deliberate review step.
android-gradle.ymlbuilds a Gradle Android project, signs it through the project's existing release configuration, and publishes the resulting APK as a Hands draft.publish-existing-apk.ymlpublishes an APK and its exact native-symbol archive produced by an earlier job or downloaded from another build system.tauri-publish.ymlbuilds signed Tauri v2 updater bundles on macOS, Linux, and Windows, then publishes one multi-platform Hands draft on a single channel.
Copy the selected file into your repository under .github/workflows/, then
replace the app slug, Gradle task, and APK path.
scripts/publish-android.sh is portable across
GitLab CI, Buildkite, Jenkins, and custom runners:
export HANDS_BEARER_TOKEN='<publisher deploy token>'
./scripts/publish-android.sh \
--app my-android-app \
--apk ./app-release.apk \
--channel preview \
--version-name 1.0.0 \
--version-code 1000000 \
--changelog ./changelog.txt \
--symbols ./hands-android-sdk-native-symbols.zip \
--native-symbols-policy required- Use an app-scoped
publisherdeploy token for CI. - Store tokens only in the CI platform's encrypted secret store.
- Examples create draft releases by default.
- Android examples validate exact native symbols before upload; validation runs locally and does not expose symbol bytes or credentials to another service.
- Never commit signing material, deploy tokens, or generated credentials.
examples/gitlab-ci/android-publish.gitlab-ci.yml— same-build APK + exact classifier transport, build-ID validation, then draft publish withhands builds publish-android.examples/gitlab-ci/ios-publish.gitlab-ci.yml— IPA + dSYM draft publish; TestFlight upload happens server-side in Hands afterwards.
examples/github-actions/ios-publish.yml— archive/export placeholder +hands builds publish-iosdraft publish. The App Store Connect credential is managed only in Hands; CI needs just the signing material and the Hands deploy token. After the draft is published, Hands uploads the build to TestFlight server-side with its stored credential (docs).
examples/github-actions/electron-publish.yml— per-platform matrix (win32/darwin/linux); each platform publishes one draft into its own platform channel (main-win32/main-darwin/main-linux) so activations never supersede another platform. Create the three channels in the app (Hands Console -> Channels) before the first run — the CLI does not create channels on publish.examples/generic/publish-electron.sh— env-driven, run once per platform, each into its own platform channel.
examples/github-actions/tauri-publish.yml— three-platform build matrix followed by one draft publish job. Tauri's signing private key stays in GitHub Actions secrets; Hands receives only the updater bundles and.sigtext.examples/generic/publish-tauri.sh— publish an already-built set oftarget=pathbundles from any CI. Unlike Electron generic-provider releases, all Tauri targets share one Hands channel because the dynamic endpoint selectstargetandarch.
examples/generic/publish-android.shexamples/generic/publish-ios.shexamples/generic/publish-electron.shexamples/generic/publish-tauri.sh
Plain shell, driven by environment variables. Keep the Android wrapper beside
this repository's scripts/ directory so it can run the shared validator; the
other platform scripts can be copied independently.