Skip to content

Commit 6b14325

Browse files
authored
Merge branch 'master' into fix/cjit-channel-false-match
2 parents e0c7a1d + 7c960a9 commit 6b14325

25 files changed

Lines changed: 1336 additions & 92 deletions

File tree

.agents/commands/release.md

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: "Create a new release: bump version, create PR, run release workflow, tag, draft release"
2+
description: "Create a new release: bump version, create PR, build mainnet, tag, draft release"
33
allowed_tools: Bash, Read, Edit, Write, Glob, Grep, AskUserQuestion, mcp__github__create_pull_request, mcp__github__list_pull_requests, mcp__github__pull_request_read, mcp__github__get_file_contents, mcp__github__update_pull_request
44
---
55

@@ -77,8 +77,6 @@ Cherry-pick the commits you need onto this branch now, then continue.
7777
```
7878
Wait for the user to confirm they are done cherry-picking before proceeding.
7979

80-
If the base is a tag that predates the release workflow changes, port the current release workflow support onto the release branch before proceeding. At minimum, the release branch/tag must contain the updated artifact naming in `.github/workflows/release.yml`, otherwise Step 7 will dispatch an old workflow and then look for an artifact name it cannot produce.
81-
8280
Finalize changelog after the release branch contains all release commits:
8381

8482
```bash
@@ -204,66 +202,28 @@ gh release edit v{newVersionName} --notes-file /tmp/release-notes.md
204202

205203
Print the path to the release notes file so the user can share it for review.
206204

207-
### 7. Run Store Release Workflow
205+
### 7. Build Mainnet Release
208206

209207
```bash
210-
release_ref="v{newVersionName}"
211-
release_artifact_dir=".ai/release-artifacts-{newVersionName}"
212-
if ! git show "$release_ref:.github/workflows/release.yml" | grep -q 'bitkit-release-$build_number-$GITHUB_RUN_NUMBER'; then
213-
echo "Release ref $release_ref does not contain the current release artifact naming." >&2
214-
echo "Port the release workflow changes onto the release branch, retag, then rerun /release." >&2
215-
exit 1
216-
fi
217-
dispatch_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
218-
gh workflow run release.yml --ref "$release_ref"
219-
run_id=""
220-
for attempt in {1..30}; do
221-
run_id="$(gh run list \
222-
--workflow release.yml \
223-
--branch "$release_ref" \
224-
--event workflow_dispatch \
225-
--created ">=$dispatch_started_at" \
226-
--limit 1 \
227-
--json databaseId \
228-
--jq '.[0].databaseId // empty')"
229-
if [ -n "$run_id" ]; then
230-
break
231-
fi
232-
sleep 5
233-
done
234-
if [ -z "$run_id" ]; then
235-
echo "Failed to find release workflow run for $release_ref" >&2
236-
exit 1
237-
fi
238-
gh run watch "$run_id" --exit-status
239-
workflow_run_url="$(gh run view "$run_id" --json url --jq .url)"
240-
run_number="$(gh run view "$run_id" --json number --jq .number)"
241-
rm -rf "$release_artifact_dir"
242-
mkdir -p "$release_artifact_dir"
243-
gh run download "$run_id" \
244-
--name "bitkit-release-{newVersionCode}-${run_number}" \
245-
--dir "$release_artifact_dir"
246-
if command -v sha256sum >/dev/null; then
247-
(cd "$release_artifact_dir" && sha256sum -c SHA256SUMS.txt)
248-
else
249-
(cd "$release_artifact_dir" && shasum -a 256 -c SHA256SUMS.txt)
250-
fi
208+
just release
251209
```
252210

253-
Expected APK path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk`
254-
Expected AAB path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}.aab`
255-
256-
Verify both files exist. If the workflow fails or the artifact checksum verification fails, stop and report the error to the user.
211+
Expected APK path: `app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk`
212+
Expected AAB path: `app/build/outputs/bundle/mainnetRelease/bitkit-mainnet-release-{newVersionCode}.aab`
213+
Expected native debug symbols path: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip`
257214

258-
Store `workflow_run_url` for the summary.
215+
Verify all three files exist. The native debug symbols file must be from the same `just release` build as the APK/AAB. Keep the build-numbered filename, e.g. `native-debug-symbols-{newVersionCode}.zip`, so it matches the APK/AAB build number. `just release` resolves upstream native debug symbol artifacts from the Rust dependency packages, merges them into the final archive, and refuses placeholder symbols from stripped packaged `.so` files.
259216

260-
### 8. Upload Workflow APK to Draft Release
217+
### 8. Upload APK and Native Symbols to Draft Release
261218

262219
```bash
263220
gh release upload v{newVersionName} \
264-
.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk
221+
app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk \
222+
app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{newVersionCode}.zip
265223
```
266224

225+
For the Play Store release, upload the AAB as usual, then upload `native-debug-symbols-{newVersionCode}.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.
226+
267227
### 9. Return to Master
268228

269229
```bash
@@ -279,16 +239,15 @@ Version bump PR: {PR URL}
279239
Release branch: release-{newVersionName}
280240
Tag: v{newVersionName}
281241
Draft release: {release URL}
282-
Release workflow: {workflow run URL}
283-
Artifacts: .ai/release-artifacts-{newVersionName}
284242
APK uploaded: bitkit-mainnet-release-{newVersionCode}-universal.apk
243+
Native debug symbols uploaded: native-debug-symbols-{newVersionCode}.zip
285244
Store release notes: .ai/release-notes-{newVersionName}.md
286245
287246
Next steps:
288247
- Share release notes with Jacobo for review
289-
- QA the workflow-built APK
290-
- Submit the workflow-built AAB to Play Store when QA passes
291-
- If patching the release branch: increment only versionCode, re-tag, rerun the release workflow, and re-upload
248+
- QA the APK
249+
- If patching the release branch: increment only versionCode, re-tag, rebuild, and re-upload
250+
- Submit to Play Store when QA passes
292251
- Publish the draft release on GitHub after store release
293252
- Merge release branch PR into master
294253
```

.github/workflows/release-internal.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ jobs:
7373
KEYSTORE_PASSWORD: ${{ secrets.INTERNAL_KEYSTORE_PASSWORD }}
7474
KEY_ALIAS: ${{ secrets.INTERNAL_KEY_ALIAS }}
7575
KEY_PASSWORD: ${{ secrets.INTERNAL_KEY_PASSWORD }}
76-
run: ./gradlew assembleMainnetRelease --no-daemon --stacktrace
76+
run: |
77+
set -euo pipefail
78+
./gradlew assembleMainnetRelease --no-daemon --stacktrace
79+
./gradlew :app:syncNativeDebugSymbolArtifacts --no-daemon --stacktrace
80+
scripts/create-native-debug-symbols.sh
7781
7882
- name: Verify native libraries are stripped
7983
run: |
@@ -124,18 +128,23 @@ jobs:
124128
artifact_dir="$RUNNER_TEMP/internal-release"
125129
mkdir -p "$artifact_dir"
126130
mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0)
131+
mapfile -d '' symbol_paths < <(find app/build/outputs/native-debug-symbols/mainnetRelease -name 'native-debug-symbols-*.zip' -print0)
127132
test "${#apk_paths[@]}" -gt 0
133+
test "${#symbol_paths[@]}" -gt 0
128134
129135
for apk_path in "${apk_paths[@]}"; do
130136
cp "$apk_path" "$artifact_dir/"
131137
done
138+
for symbol_path in "${symbol_paths[@]}"; do
139+
cp "$symbol_path" "$artifact_dir/"
140+
done
132141
133142
apk_file="$(basename "${apk_paths[0]}")"
134143
build_number="${apk_file#bitkit-mainnet-release-}"
135144
build_number="${build_number%%-*}"
136145
[[ "$build_number" =~ ^[0-9]+$ ]]
137146
138-
(cd "$artifact_dir" && sha256sum -- *.apk > SHA256SUMS.txt)
147+
(cd "$artifact_dir" && sha256sum -- *.apk native-debug-symbols-*.zip > SHA256SUMS.txt)
139148
echo "artifact_name=bitkit-release-internal-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT"
140149
echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT"
141150

.github/workflows/release.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ jobs:
7474
KEYSTORE_PASSWORD: ${{ secrets.BITKIT_KEYSTORE_PASSWORD }}
7575
KEY_ALIAS: ${{ secrets.BITKIT_KEY_ALIAS }}
7676
KEY_PASSWORD: ${{ secrets.BITKIT_KEY_PASSWORD }}
77-
run: ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace
77+
run: |
78+
set -euo pipefail
79+
./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace
80+
./gradlew :app:syncNativeDebugSymbolArtifacts --no-daemon --stacktrace
81+
scripts/create-native-debug-symbols.sh
7882
7983
- name: Verify native libraries are stripped
8084
run: |
@@ -148,22 +152,27 @@ jobs:
148152
mkdir -p "$artifact_dir"
149153
mapfile -d '' bundle_paths < <(find app/build/outputs/bundle/mainnetRelease -name 'bitkit-mainnet-release-*.aab' -print0)
150154
mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0)
155+
mapfile -d '' symbol_paths < <(find app/build/outputs/native-debug-symbols/mainnetRelease -name 'native-debug-symbols-*.zip' -print0)
151156
test "${#bundle_paths[@]}" -gt 0
152157
test "${#apk_paths[@]}" -gt 0
158+
test "${#symbol_paths[@]}" -gt 0
153159
154160
for bundle_path in "${bundle_paths[@]}"; do
155161
cp "$bundle_path" "$artifact_dir/"
156162
done
157163
for apk_path in "${apk_paths[@]}"; do
158164
cp "$apk_path" "$artifact_dir/"
159165
done
166+
for symbol_path in "${symbol_paths[@]}"; do
167+
cp "$symbol_path" "$artifact_dir/"
168+
done
160169
161170
apk_file="$(basename "${apk_paths[0]}")"
162171
build_number="${apk_file#bitkit-mainnet-release-}"
163172
build_number="${build_number%%-*}"
164173
[[ "$build_number" =~ ^[0-9]+$ ]]
165174
166-
(cd "$artifact_dir" && sha256sum -- *.aab *.apk > SHA256SUMS.txt)
175+
(cd "$artifact_dir" && sha256sum -- *.aab *.apk native-debug-symbols-*.zip > SHA256SUMS.txt)
167176
echo "artifact_name=bitkit-release-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT"
168177
echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT"
169178

Justfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ build task="assembleDevDebug":
173173
{{ gradle }} {{ task }}
174174

175175
release:
176+
#!/usr/bin/env sh
177+
set -eu
178+
symbols_dir="app/build/outputs/native-debug-symbols/mainnetRelease"
179+
rm -f "$symbols_dir"/native-debug-symbols*.zip
176180
NDK_VERSION={{ ndk_ver }} {{ gradle }} assembleMainnetRelease bundleMainnetRelease
181+
NDK_VERSION={{ ndk_ver }} {{ gradle }} :app:syncNativeDebugSymbolArtifacts
182+
scripts/create-native-debug-symbols.sh
183+
symbols="$(find "$symbols_dir" -maxdepth 1 -name 'native-debug-symbols-*.zip' -type f | sort | tail -n 1)"
184+
echo "Attach this exact file to GitHub releases, upload it to Play Console for this release, and verify Play lists it: $symbols"
177185
178186
install:
179187
{{ gradle }} installDevDebug

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,22 @@ To build the mainnet flavor for release run:
180180
just release
181181
```
182182

183+
`just release` builds the mainnet APK, Play Store AAB, resolves upstream native debug symbol artifacts, and validates the native debug symbols archive.
184+
185+
Release artifacts:
186+
187+
- APK: `app/build/outputs/apk/mainnet/release/`
188+
- AAB: `app/build/outputs/bundle/mainnetRelease/`
189+
- Native debug symbols: `app/build/outputs/native-debug-symbols/mainnetRelease/native-debug-symbols-{versionCode}.zip`
190+
191+
The native debug symbols archive must come from the same `just release` build as the APK/AAB being published. Keep the build-numbered filename, e.g. `native-debug-symbols-182.zip`, so it matches the APK/AAB build number. Native Rust dependencies publish stripped release AARs for app size and separate `native-debug-symbols` classifier artifacts for crash symbolication; `just release` merges those upstream symbol artifacts into the final archive and refuses placeholder symbols from stripped packaged `.so` files.
192+
193+
For Play Store releases, upload the AAB as usual, then upload `native-debug-symbols-{versionCode}.zip` for that exact version/build in Play Console: App bundle explorer → Downloads → Assets. Verify Play lists the native debug symbols after upload.
194+
195+
Keep the release-built `native-debug-symbols-{versionCode}.zip` in GitHub releases or internal release storage. Play Console may only show delete/replace controls after upload, which is enough for release verification.
196+
197+
For GitHub releases, attach `native-debug-symbols-{versionCode}.zip` alongside the APK so native crashes from GitHub-distributed builds can be symbolicated later.
198+
183199
#### Android App Bundle (AAB)
184200

185201
`just release` builds both the mainnet APK and Play Store AAB. AAB is generated in `app/build/outputs/bundle/mainnetRelease/`.

app/build.gradle.kts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
22
import com.android.build.gradle.internal.tasks.FinalizeBundleTask
33
import io.gitlab.arturbosch.detekt.Detekt
4+
import org.gradle.api.artifacts.MinimalExternalModuleDependency
5+
import org.gradle.api.provider.Provider
6+
import org.gradle.api.tasks.Sync
47
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
58
import org.gradle.api.tasks.testing.logging.TestLogEvent
69
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@@ -232,7 +235,7 @@ android {
232235
)
233236
signingConfig = signingConfigs.getByName("release")
234237
ndk {
235-
debugSymbolLevel = "SYMBOL_TABLE"
238+
debugSymbolLevel = "FULL"
236239
// noinspection ChromeOsAbiSupport
237240
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
238241
}
@@ -318,6 +321,31 @@ composeCompiler {
318321
reportsDestination = layout.buildDirectory.dir("compose_compiler")
319322
}
320323

324+
val nativeDebugSymbols by configurations.creating {
325+
isCanBeConsumed = false
326+
isCanBeResolved = true
327+
}
328+
329+
fun Provider<MinimalExternalModuleDependency>.nativeDebugSymbolsArtifact(): String {
330+
val dependency = get()
331+
val version = dependency.versionConstraint.requiredVersion
332+
.ifBlank { dependency.versionConstraint.preferredVersion }
333+
334+
require(version.isNotBlank()) {
335+
"Native debug symbols dependency '${dependency.module}' must declare an explicit version."
336+
}
337+
338+
return "${dependency.module.group}:${dependency.module.name}:$version:native-debug-symbols@zip"
339+
}
340+
341+
val syncNativeDebugSymbolArtifacts by tasks.registering(Sync::class) {
342+
group = "build"
343+
description = "Downloads native debug symbol archives for release native dependencies."
344+
345+
from(nativeDebugSymbols)
346+
into(layout.buildDirectory.dir("intermediates/native-debug-symbol-artifacts"))
347+
}
348+
321349
dependencies {
322350
implementation(fileTree("libs") { include("*.aar", "*.jar") })
323351
implementation(libs.jna) { artifact { type = "aar" } }
@@ -343,6 +371,9 @@ dependencies {
343371
implementation(libs.bitkit.core)
344372
implementation(libs.paykit)
345373
implementation(libs.vss.client)
374+
nativeDebugSymbols(libs.bitkit.core.nativeDebugSymbolsArtifact())
375+
nativeDebugSymbols(libs.ldk.node.android.nativeDebugSymbolsArtifact())
376+
nativeDebugSymbols(libs.vss.client.nativeDebugSymbolsArtifact())
346377
// Firebase
347378
implementation(platform(libs.firebase.bom))
348379
implementation(libs.firebase.messaging)

app/src/main/java/to/bitkit/models/HwWallet.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import androidx.compose.runtime.Immutable
44
import androidx.compose.runtime.Stable
55
import com.synonym.bitkitcore.Activity
66
import kotlinx.collections.immutable.ImmutableList
7+
import kotlinx.collections.immutable.ImmutableSet
8+
import kotlinx.collections.immutable.persistentSetOf
79
import kotlinx.serialization.Serializable
810

911
/** A paired hardware wallet tracked as a watch-only balance. */
@@ -16,6 +18,7 @@ data class HwWallet(
1618
val isConnected: Boolean,
1719
val balanceSats: ULong,
1820
val activities: ImmutableList<Activity>,
21+
val deviceIds: ImmutableSet<String> = persistentSetOf(id),
1922
)
2023

2124
/** Serializable per-device balance snapshot carried by [BalanceState]. */

app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.synonym.bitkitcore.WatcherEvent
99
import kotlinx.collections.immutable.ImmutableList
1010
import kotlinx.collections.immutable.persistentListOf
1111
import kotlinx.collections.immutable.toImmutableList
12+
import kotlinx.collections.immutable.toImmutableSet
1213
import kotlinx.coroutines.CoroutineDispatcher
1314
import kotlinx.coroutines.CoroutineScope
1415
import kotlinx.coroutines.SupervisorJob
@@ -40,6 +41,7 @@ import to.bitkit.models.safe
4041
import to.bitkit.models.toAccountType
4142
import to.bitkit.models.toAddressType
4243
import to.bitkit.models.toCoreNetwork
44+
import to.bitkit.utils.AppError
4345
import to.bitkit.utils.Logger
4446
import javax.inject.Inject
4547
import javax.inject.Singleton
@@ -109,6 +111,34 @@ class HwWalletRepo @Inject constructor(
109111

110112
fun cancelPairingCode() = trezorRepo.cancelPairingCode()
111113

114+
/**
115+
* Removes a paired hardware wallet: stops its watchers and forgets every device entry
116+
* that tracks the same wallet. The same physical device paired over both bluetooth and
117+
* usb is stored once per transport but shares an xpub-derived identity, so forgetting a
118+
* single id would leave the tile reappearing through the other transport.
119+
*/
120+
suspend fun removeDevice(deviceId: String): Result<Unit> = withContext(ioDispatcher) {
121+
runCatching {
122+
val knownDevices = hwWalletStore.loadKnownDevices()
123+
val target = knownDevices.find { it.id == deviceId }
124+
val ids = when (target) {
125+
null -> setOf(deviceId)
126+
else -> knownDevices.filter { it.walletKey == target.walletKey }.map { it.id }.toSet()
127+
}
128+
activeWatchers.toList()
129+
.filter { it.toDeviceId() in ids }
130+
.forEach {
131+
if (!stopActiveWatcher(it)) throw AppError("Failed to stop hardware wallet watcher '$it'")
132+
}
133+
val failures = ids.mapNotNull { trezorRepo.forgetDevice(it).exceptionOrNull() }
134+
val remaining = hwWalletStore.loadKnownDevices().map { it.id }.toSet()
135+
failures.firstOrNull()?.let { throw it }
136+
check(ids.none { it in remaining }) { "Hardware wallet '$deviceId' still present after removal" }
137+
}.onFailure {
138+
watcherSyncRequests.tryEmit(Unit)
139+
}
140+
}
141+
112142
val wallets: StateFlow<ImmutableList<HwWallet>> = combine(
113143
hwWalletStore.data,
114144
trezorRepo.state,
@@ -135,11 +165,16 @@ class HwWalletRepo @Inject constructor(
135165
activities = deviceWatchers
136166
.toMergedActivities()
137167
.toImmutableList(),
168+
deviceIds = ids.toImmutableSet(),
138169
)
139170
}
140171
.toImmutableList()
141172
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
142173

174+
val walletsLoaded: StateFlow<Boolean> = hwWalletStore.data
175+
.map { true }
176+
.stateIn(scope, SharingStarted.Eagerly, false)
177+
143178
val totalSats: StateFlow<ULong> = wallets
144179
.map { wallets -> wallets.fold(0uL) { acc, wallet -> acc + wallet.balanceSats } }
145180
.stateIn(scope, SharingStarted.Eagerly, 0uL)

app/src/main/java/to/bitkit/repositories/TrezorRepo.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,11 +652,15 @@ class TrezorRepo @Inject constructor(
652652
TrezorDebugLog.log("FORGET", "Clearing credentials...")
653653
trezorTransport.clearDeviceCredential(deviceId)
654654
val clearCredentialsResult = runCatching { trezorService.clearCredentials(deviceId) }
655-
val updated = _state.value.knownDevices.filter { it.id != deviceId }
655+
val knownDevices = (_state.value.knownDevices + loadKnownDevices()).distinctBy { it.id }
656+
val updated = knownDevices.filter { it.id != deviceId }
656657
saveKnownDevices(updated)
657658
_state.update { it.copy(knownDevices = updated.toImmutableList()) }
658-
disconnectResult.getOrThrow()
659659
clearCredentialsResult.getOrThrow()
660+
disconnectResult.onFailure {
661+
TrezorDebugLog.log("FORGET", "Ignored disconnect failure: ${it.message}")
662+
Logger.warn("Ignored disconnect failure while forgetting device '$deviceId'", it, context = TAG)
663+
}
660664
TrezorDebugLog.log("FORGET", "Device forgotten successfully")
661665
Logger.info("Forgot device: '$deviceId'", context = TAG)
662666
}.onFailure { e ->

0 commit comments

Comments
 (0)