diff --git a/.agents/commands/release.md b/.agents/commands/release.md index 532790c65d..da2fe615ba 100644 --- a/.agents/commands/release.md +++ b/.agents/commands/release.md @@ -1,5 +1,5 @@ --- -description: "Create a new release: bump version, create PR, build mainnet, tag, draft release" +description: "Create a new release: bump version, create PR, run release workflow, tag, draft release" 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 --- @@ -77,6 +77,8 @@ Cherry-pick the commits you need onto this branch now, then continue. ``` Wait for the user to confirm they are done cherry-picking before proceeding. +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. + Finalize changelog after the release branch contains all release commits: ```bash @@ -202,22 +204,64 @@ gh release edit v{newVersionName} --notes-file /tmp/release-notes.md Print the path to the release notes file so the user can share it for review. -### 7. Build Mainnet Release +### 7. Run Store Release Workflow ```bash -just release +release_ref="v{newVersionName}" +release_artifact_dir=".ai/release-artifacts-{newVersionName}" +if ! git show "$release_ref:.github/workflows/release.yml" | grep -q 'bitkit-release-$build_number-$GITHUB_RUN_NUMBER'; then + echo "Release ref $release_ref does not contain the current release artifact naming." >&2 + echo "Port the release workflow changes onto the release branch, retag, then rerun /release." >&2 + exit 1 +fi +dispatch_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +gh workflow run release.yml --ref "$release_ref" +run_id="" +for attempt in {1..30}; do + run_id="$(gh run list \ + --workflow release.yml \ + --branch "$release_ref" \ + --event workflow_dispatch \ + --created ">=$dispatch_started_at" \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId // empty')" + if [ -n "$run_id" ]; then + break + fi + sleep 5 +done +if [ -z "$run_id" ]; then + echo "Failed to find release workflow run for $release_ref" >&2 + exit 1 +fi +gh run watch "$run_id" --exit-status +workflow_run_url="$(gh run view "$run_id" --json url --jq .url)" +run_number="$(gh run view "$run_id" --json number --jq .number)" +rm -rf "$release_artifact_dir" +mkdir -p "$release_artifact_dir" +gh run download "$run_id" \ + --name "bitkit-release-{newVersionCode}-${run_number}" \ + --dir "$release_artifact_dir" +if command -v sha256sum >/dev/null; then + (cd "$release_artifact_dir" && sha256sum -c SHA256SUMS.txt) +else + (cd "$release_artifact_dir" && shasum -a 256 -c SHA256SUMS.txt) +fi ``` -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 APK path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk` +Expected AAB path: `.ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}.aab` + +Verify both files exist. If the workflow fails or the artifact checksum verification fails, stop and report the error to the user. -Verify both files exist. If the build fails, stop and report the error to the user. +Store `workflow_run_url` for the summary. -### 8. Upload APK to Draft Release +### 8. Upload Workflow APK to Draft Release ```bash gh release upload v{newVersionName} \ - app/build/outputs/apk/mainnet/release/bitkit-mainnet-release-{newVersionCode}-universal.apk + .ai/release-artifacts-{newVersionName}/bitkit-mainnet-release-{newVersionCode}-universal.apk ``` ### 9. Return to Master @@ -235,14 +279,16 @@ Version bump PR: {PR URL} Release branch: release-{newVersionName} Tag: v{newVersionName} Draft release: {release URL} +Release workflow: {workflow run URL} +Artifacts: .ai/release-artifacts-{newVersionName} APK uploaded: bitkit-mainnet-release-{newVersionCode}-universal.apk Store release notes: .ai/release-notes-{newVersionName}.md Next steps: - Share release notes with Jacobo for review -- QA the APK -- If patching the release branch: increment only versionCode, re-tag, rebuild, and re-upload -- Submit to Play Store when QA passes +- QA the workflow-built APK +- Submit the workflow-built AAB to Play Store when QA passes +- If patching the release branch: increment only versionCode, re-tag, rerun the release workflow, and re-upload - Publish the draft release on GitHub after store release - Merge release branch PR into master ``` diff --git a/.github/workflows/release-internal.yml b/.github/workflows/release-internal.yml index 89c042bcb9..4f419bd702 100644 --- a/.github/workflows/release-internal.yml +++ b/.github/workflows/release-internal.yml @@ -10,10 +10,10 @@ concurrency: env: TERM: xterm-256color FORCE_COLOR: 1 + NDK_VERSION: 28.1.13356709 jobs: build-internal: - if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest timeout-minutes: 45 environment: release-internal @@ -35,6 +35,16 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + - name: Setup Android NDK + run: | + set -euo pipefail + android_sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + test -n "$android_sdk_root" + sdkmanager_path="$android_sdk_root/cmdline-tools/latest/bin/sdkmanager" + test -x "$sdkmanager_path" + yes | "$sdkmanager_path" --licenses >/dev/null || true + "$sdkmanager_path" --install "ndk;$NDK_VERSION" + - name: Decode mainnet release google-services.json env: MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64 }} @@ -65,6 +75,33 @@ jobs: KEY_PASSWORD: ${{ secrets.INTERNAL_KEY_PASSWORD }} run: ./gradlew assembleMainnetRelease --no-daemon --stacktrace + - name: Verify native libraries are stripped + run: | + set -euo pipefail + android_sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + test -n "$android_sdk_root" + llvm_readelf_candidates=("$android_sdk_root/ndk/$NDK_VERSION"/toolchains/llvm/prebuilt/*/bin/llvm-readelf) + llvm_readelf_path="${llvm_readelf_candidates[0]}" + test -x "$llvm_readelf_path" + + native_count=0 + extract_root="$(mktemp -d)" + trap 'rm -rf "$extract_root"' EXIT + while IFS= read -r -d '' artifact_path; do + extract_dir="$extract_root/$(basename "$artifact_path")" + mkdir -p "$extract_dir" + unzip -q "$artifact_path" 'lib/*/*.so' -d "$extract_dir" 2>/dev/null || true + while IFS= read -r -d '' native_path; do + native_count=$((native_count + 1)) + if "$llvm_readelf_path" -S "$native_path" | grep -q '\.debug_'; then + native_entry="${native_path#"$extract_dir"/}" + echo "Native library contains debug sections: $artifact_path:$native_entry" + exit 1 + fi + done < <(find "$extract_dir" -name '*.so' -print0) + done < <(find app/build/outputs/apk/mainnet/release -type f -name '*.apk' -print0) + test "$native_count" -gt 0 + - name: Verify internal release signature run: | set -euo pipefail @@ -86,14 +123,25 @@ jobs: set -euo pipefail artifact_dir="$RUNNER_TEMP/internal-release" mkdir -p "$artifact_dir" - find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0 | - xargs -0 -I {} cp {} "$artifact_dir/" - (cd "$artifact_dir" && sha256sum *.apk > SHA256SUMS.txt) + mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + test "${#apk_paths[@]}" -gt 0 + + for apk_path in "${apk_paths[@]}"; do + cp "$apk_path" "$artifact_dir/" + done + + apk_file="$(basename "${apk_paths[0]}")" + build_number="${apk_file#bitkit-mainnet-release-}" + build_number="${build_number%%-*}" + [[ "$build_number" =~ ^[0-9]+$ ]] + + (cd "$artifact_dir" && sha256sum -- *.apk > SHA256SUMS.txt) + echo "artifact_name=bitkit-release-internal-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" - name: Upload internal artifacts uses: actions/upload-artifact@v6 with: - name: bitkit-internal-release-${{ github.run_number }} + name: ${{ steps.artifacts.outputs.artifact_name }} path: ${{ steps.artifacts.outputs.artifact_dir }} retention-days: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54c810537e..bde387774c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,10 +10,11 @@ concurrency: env: TERM: xterm-256color FORCE_COLOR: 1 + NDK_VERSION: 28.1.13356709 jobs: build-release: - if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest timeout-minutes: 45 environment: release @@ -35,6 +36,16 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + - name: Setup Android NDK + run: | + set -euo pipefail + android_sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + test -n "$android_sdk_root" + sdkmanager_path="$android_sdk_root/cmdline-tools/latest/bin/sdkmanager" + test -x "$sdkmanager_path" + yes | "$sdkmanager_path" --licenses >/dev/null || true + "$sdkmanager_path" --install "ndk;$NDK_VERSION" + - name: Decode mainnet release google-services.json env: MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64 }} @@ -65,6 +76,33 @@ jobs: KEY_PASSWORD: ${{ secrets.BITKIT_KEY_PASSWORD }} run: ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace + - name: Verify native libraries are stripped + run: | + set -euo pipefail + android_sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + test -n "$android_sdk_root" + llvm_readelf_candidates=("$android_sdk_root/ndk/$NDK_VERSION"/toolchains/llvm/prebuilt/*/bin/llvm-readelf) + llvm_readelf_path="${llvm_readelf_candidates[0]}" + test -x "$llvm_readelf_path" + + native_count=0 + extract_root="$(mktemp -d)" + trap 'rm -rf "$extract_root"' EXIT + while IFS= read -r -d '' artifact_path; do + extract_dir="$extract_root/$(basename "$artifact_path")" + mkdir -p "$extract_dir" + unzip -q "$artifact_path" 'lib/*/*.so' 'base/lib/*/*.so' -d "$extract_dir" 2>/dev/null || true + while IFS= read -r -d '' native_path; do + native_count=$((native_count + 1)) + if "$llvm_readelf_path" -S "$native_path" | grep -q '\.debug_'; then + native_entry="${native_path#"$extract_dir"/}" + echo "Native library contains debug sections: $artifact_path:$native_entry" + exit 1 + fi + done < <(find "$extract_dir" -name '*.so' -print0) + done < <(find app/build/outputs/apk/mainnet/release app/build/outputs/bundle/mainnetRelease -type f \( -name '*.apk' -o -name '*.aab' \) -print0) + test "$native_count" -gt 0 + - name: Verify release signatures run: | set -euo pipefail @@ -108,16 +146,30 @@ jobs: set -euo pipefail artifact_dir="$RUNNER_TEMP/release" mkdir -p "$artifact_dir" - find app/build/outputs/bundle/mainnetRelease -name 'bitkit-mainnet-release-*.aab' -print0 | - xargs -0 -I {} cp {} "$artifact_dir/" - find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0 | - xargs -0 -I {} cp {} "$artifact_dir/" - (cd "$artifact_dir" && sha256sum *.aab *.apk > SHA256SUMS.txt) + mapfile -d '' bundle_paths < <(find app/build/outputs/bundle/mainnetRelease -name 'bitkit-mainnet-release-*.aab' -print0) + mapfile -d '' apk_paths < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + test "${#bundle_paths[@]}" -gt 0 + test "${#apk_paths[@]}" -gt 0 + + for bundle_path in "${bundle_paths[@]}"; do + cp "$bundle_path" "$artifact_dir/" + done + for apk_path in "${apk_paths[@]}"; do + cp "$apk_path" "$artifact_dir/" + done + + apk_file="$(basename "${apk_paths[0]}")" + build_number="${apk_file#bitkit-mainnet-release-}" + build_number="${build_number%%-*}" + [[ "$build_number" =~ ^[0-9]+$ ]] + + (cd "$artifact_dir" && sha256sum -- *.aab *.apk > SHA256SUMS.txt) + echo "artifact_name=bitkit-release-$build_number-$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" - name: Upload release artifacts uses: actions/upload-artifact@v6 with: - name: bitkit-release-${{ github.run_number }} + name: ${{ steps.artifacts.outputs.artifact_name }} path: ${{ steps.artifacts.outputs.artifact_dir }} retention-days: 30 diff --git a/Justfile b/Justfile index 0b42d0dfd7..1d282ff82c 100644 --- a/Justfile +++ b/Justfile @@ -3,6 +3,7 @@ set dotenv-filename := ".env" set windows-shell := ["sh", "-cu"] gradle := "./gradlew" +ndk_ver := "28.1.13356709" default: @just list @@ -45,16 +46,29 @@ init: compile: {{ gradle }} compileDevDebugKotlin -run mode="": +run mode="" logs="": #!/usr/bin/env sh set -eu app_id="to.bitkit.dev" app_dir="app/build/outputs/apk/dev/debug" mode="{{ mode }}" + logs="{{ logs }}" + attach_logs=false + if [ "$mode" = "logs" ]; then + attach_logs=true + mode="" + fi + if [ -n "$logs" ]; then + if [ "$logs" != "logs" ]; then + echo "usage: just run [docker] [logs]" >&2 + exit 1 + fi + attach_logs=true + fi if [ -n "$mode" ] && [ "$mode" != "docker" ]; then - echo "usage: just run [docker]" >&2 + echo "usage: just run [docker] [logs]" >&2 exit 1 fi @@ -136,6 +150,11 @@ run mode="": adb -s "$device_id" shell am force-stop "$app_id" adb -s "$device_id" shell monkey -p "$app_id" -c android.intent.category.LAUNCHER 1 >/dev/null + if [ "$attach_logs" != "true" ]; then + echo "Launched $app_id" + exit 0 + fi + pid="$( adb -s "$device_id" shell pidof -s "$app_id" 2>/dev/null \ | tr -d '\r' \ @@ -154,7 +173,7 @@ build task="assembleDevDebug": {{ gradle }} {{ task }} release: - {{ gradle }} assembleMainnetRelease bundleMainnetRelease + NDK_VERSION={{ ndk_ver }} {{ gradle }} assembleMainnetRelease bundleMainnetRelease install: {{ gradle }} installDevDebug diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5542ddbdee..13af5ccbb7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local" val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288" val trezorBridgeEnv = System.getenv("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false" val trezorBridgeUrlEnv = System.getenv("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325" +val requestedNdkVersion = System.getenv("NDK_VERSION")?.takeIf { it.isNotBlank() } val androidTestAnnotationPackage = "to.bitkit.test.annotations" val androidTestTaskPrefix = "connectedDevDebug" val androidTestTaskSuffix = "AndroidTest" @@ -145,6 +146,7 @@ val bitkitAndroidTestAnnotation = bitkitAndroidTestAnnotationName?.let { android { namespace = "to.bitkit" compileSdk = 36 + requestedNdkVersion?.let { ndkVersion = it } defaultConfig { applicationId = "to.bitkit" minSdk = 28 @@ -230,6 +232,7 @@ android { ) signingConfig = signingConfigs.getByName("release") ndk { + debugSymbolLevel = "SYMBOL_TABLE" // noinspection ChromeOsAbiSupport abiFilters += listOf("armeabi-v7a", "arm64-v8a") }