Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .agents/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,20 @@ just release

Expected APK path: `app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk`
Expected AAB path: `app/build/outputs/bundle/mainnetRelease/bitkit-mainnet-release-{newVersionCode}.aab`
Expected native debug symbols path: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip`

Verify both files exist. If the build fails, stop and report the error to the user.
Verify all three files exist. The native debug symbols file must be from the same `just release` build as the APK/AAB. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit a usable archive because native dependency metadata is already stripped, `just release` fails instead of creating a placeholder zip from stripped `.so` files. Stop the release and publish or consume native dependencies with usable debug metadata first.

### 8. Upload APK to Draft Release
### 8. Upload APK and Native Symbols to Draft Release

```bash
gh release upload v{newVersionName} \
app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk
app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk \
app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip
```

For the Play Store release, upload the AAB as usual, then upload `native-debug-symbols.zip` for the exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload. Keep the release-built archive in GitHub releases or internal release storage; Play Console may only show delete/replace controls after upload, which is enough for release verification.

### 9. Return to Master

```bash
Expand All @@ -236,6 +240,7 @@ Release branch: release-{newVersionName}
Tag: v{newVersionName}
Draft release: {release URL}
APK uploaded: bitkit-mainnet-release-{newVersionCode}-universal.apk
Native debug symbols uploaded: native-debug-symbols.zip
Store release notes: .ai/release-notes-{newVersionName}.md

Next steps:
Expand Down
6 changes: 6 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,13 @@ build task="assembleDevDebug":
{{ gradle }} {{ task }}

release:
#!/usr/bin/env sh
set -eu
symbols="app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip"
rm -f "$symbols"
{{ gradle }} assembleMainnetRelease bundleMainnetRelease
scripts/create-native-debug-symbols.sh
echo "Attach this exact file to GitHub releases, upload it to Play Console for this release, and verify Play lists it."

install:
{{ gradle }} installDevDebug
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ To build the mainnet flavor for release run:
just release
```

`just release` builds the mainnet APK, Play Store AAB, and validates the native debug symbols archive.

Release artifacts:

- APK: `app/build/outputs/apk/mainnet/release/`
- AAB: `app/build/outputs/bundle/mainnetRelease/`
- Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip`

The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the filename `native-debug-symbols.zip`. If Android Gradle Plugin cannot emit a usable archive because native dependency metadata is already stripped, `just release` fails instead of creating a placeholder zip from stripped `.so` files. Stop the release and publish or consume native dependencies with usable debug metadata first.

For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload.

Keep the release-built `native-debug-symbols.zip` in GitHub releases or internal release storage. Play Console may only show delete/replace controls after upload, which is enough for release verification.

For GitHub releases, attach `native-debug-symbols.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later.

#### Android App Bundle (AAB)

`just release` builds both the mainnet APK and Play Store AAB. AAB is generated in `app/build/outputs/bundle/mainnetRelease/`.
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ android {
)
signingConfig = signingConfigs.getByName("release")
ndk {
debugSymbolLevel = "FULL"
// noinspection ChromeOsAbiSupport
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
Expand Down
118 changes: 118 additions & 0 deletions app/src/test/java/to/bitkit/build/NativeReleaseConfigTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package to.bitkit.build

import kotlin.io.path.Path
import kotlin.io.path.exists
import kotlin.io.path.readText
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class NativeReleaseConfigTest {

private val repoRoot = generateSequence(
Path(requireNotNull(System.getProperty("user.dir")) { "user.dir is required" }),
) { it.parent }
.first { it.resolve("gradle/libs.versions.toml").exists() }

@Test
fun `release build requests full native debug symbols`() {
val buildFile = repoRoot.resolve("app/build.gradle.kts").readText()

assertTrue(
buildFile.contains("""debugSymbolLevel = "FULL""""),
"Release builds must request full native debug symbols for Play crash symbolication.",
)
}

@Test
fun `release recipe verifies native debug symbols archive`() {
val justfile = repoRoot.resolve("Justfile").readText()

assertTrue(
justfile.contains(
"""rm -f "${'$'}symbols"""",
),
"Release builds must remove stale native debug symbols before rebuilding.",
)
assertTrue(
justfile.contains("scripts/create-native-debug-symbols.sh"),
"Release builds must create the native debug symbols archive before publishing.",
)
assertTrue(
justfile.contains("Attach this exact file to GitHub releases"),
"Release builds must tell the releaser to attach native debug symbols.",
)
assertTrue(
justfile.contains("upload it to Play Console for this release"),
"Release builds must tell the releaser to upload native debug symbols to Play.",
)
assertFalse(
justfile.contains("download"),
"Release builds should keep native debug symbols in release storage.",
)
}

@Test
fun `release command uploads native debug symbols archive`() {
val releaseCommand = repoRoot.resolve(".agents/commands/release.md").readText()

assertTrue(
releaseCommand.contains(
"app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols.zip",
),
"Release command must include the native debug symbols archive path.",
)
assertTrue(
releaseCommand.contains("Native debug symbols uploaded: native-debug-symbols.zip"),
"Release command summary must report the native debug symbols archive.",
)
assertFalse(
releaseCommand.contains("Play " + "did not"),
"Release command should use current Play native symbol wording.",
)
assertTrue(
releaseCommand.contains("fails instead of creating a placeholder zip from stripped `.so` files"),
"Release command must fail instead of publishing fake native debug symbols.",
)
assertTrue(
releaseCommand.contains("Play Console may only show delete/replace controls"),
"Release command must document the verified Play Console behavior.",
)
}

@Test
fun `native debug symbols script rejects stripped release libraries`() {
val symbolsScript = repoRoot.resolve("scripts/create-native-debug-symbols.sh").readText()

assertTrue(
symbolsScript.contains(
"app/build/outputs/native-debug-symbols/${'$'}variant/native-debug-symbols.zip",
),
"Native debug symbols script must write the canonical archive path.",
)
assertTrue(
symbolsScript.contains("arm64-v8a armeabi-v7a"),
"Native debug symbols script must archive Play release ABIs.",
)
assertTrue(
symbolsScript.contains("zip -qr"),
"Native debug symbols script must create a zip archive.",
)
assertTrue(
symbolsScript.contains("""required_libs="libbitkitcore.so libldk_node.so libvss_rust_client_ffi.so""""),
"Native debug symbols script must validate Rust native libraries.",
)
assertTrue(
symbolsScript.contains("""archive_symbol_suffixes=".dbg .sym""""),
"Native debug symbols script must accept AGP native debug symbol entry suffixes.",
)
assertTrue(
symbolsScript.contains("""grep -Eq '\.(symtab|debug_|gnu_debugdata)'"""),
"Native debug symbols script must validate usable debug metadata before zipping.",
)
assertTrue(
symbolsScript.contains("Refusing to create '${'$'}output' from stripped native libraries."),
"Native debug symbols script must refuse placeholder archives.",
)
}
}
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version = "1
appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" }
biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" }
bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.67" }
bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.70" }
paykit = { module = "com.synonym:paykit-android", version = "0.1.0-rc8" }
bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" }
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
Expand Down Expand Up @@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.46" }
ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.48" }
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
Expand All @@ -88,7 +88,7 @@ test-junit-ext = { module = "androidx.test.ext:junit", version = "1.3.0" }
test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.2.2" }
test-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" }
test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.2.1" }
vss-client = { module = "com.synonym:vss-client-android", version = "0.5.12" }
vss-client = { module = "com.synonym:vss-client-android", version = "0.5.17" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" }
zxing = { module = "com.google.zxing:core", version = "3.5.4" }
lottie = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" }
Expand Down
Loading
Loading