From a4e415b766e8dba668c818ab94c58a1012f625b4 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 15 Jun 2026 12:28:08 +0200 Subject: [PATCH] ci: add release workflows --- .github/workflows/ci.yml | 9 +- .github/workflows/e2e.yml | 18 +++- .github/workflows/e2e_migration.yml | 9 +- .github/workflows/release-internal.yml | 99 ++++++++++++++++++++ .github/workflows/release.yml | 123 +++++++++++++++++++++++++ .github/workflows/ui-tests.yml | 9 +- .gitignore | 2 + README.md | 10 +- app/google-services.json | 48 ++++++++++ settings.gradle.kts | 6 +- 10 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/release-internal.yml create mode 100644 .github/workflows/release.yml create mode 100644 app/google-services.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d33373c..8f1e8b836 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,14 @@ jobs: uses: gradle/actions/setup-gradle@v5 - name: Decode google-services.json - run: echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/google-services.json + run: | + set -euo pipefail + if [ -z "${GOOGLE_SERVICES_JSON_BASE64:-}" ]; then + echo "GOOGLE_SERVICES_JSON_BASE64 is empty; using checked-in placeholder." + exit 0 + fi + mkdir -p app/src/debug + printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/src/debug/google-services.json env: GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index be8f9b14d..5cf2252db 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -57,7 +57,14 @@ jobs: uses: gradle/actions/setup-gradle@v5 - name: Decode google-services.json - run: echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/google-services.json + run: | + set -euo pipefail + if [ -z "${GOOGLE_SERVICES_JSON_BASE64:-}" ]; then + echo "GOOGLE_SERVICES_JSON_BASE64 is empty; using checked-in placeholder." + exit 0 + fi + mkdir -p app/src/debug + printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/src/debug/google-services.json env: GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} @@ -100,7 +107,14 @@ jobs: uses: gradle/actions/setup-gradle@v5 - name: Decode google-services.json - run: echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/google-services.json + run: | + set -euo pipefail + if [ -z "${GOOGLE_SERVICES_JSON_BASE64:-}" ]; then + echo "GOOGLE_SERVICES_JSON_BASE64 is empty; using checked-in placeholder." + exit 0 + fi + mkdir -p app/src/debug + printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/src/debug/google-services.json env: GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} diff --git a/.github/workflows/e2e_migration.yml b/.github/workflows/e2e_migration.yml index 373f3236e..96b671036 100644 --- a/.github/workflows/e2e_migration.yml +++ b/.github/workflows/e2e_migration.yml @@ -37,7 +37,14 @@ jobs: uses: gradle/actions/setup-gradle@v5 - name: Decode google-services.json - run: echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/google-services.json + run: | + set -euo pipefail + if [ -z "${GOOGLE_SERVICES_JSON_BASE64:-}" ]; then + echo "GOOGLE_SERVICES_JSON_BASE64 is empty; using checked-in placeholder." + exit 0 + fi + mkdir -p app/src/debug + printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/src/debug/google-services.json env: GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} diff --git a/.github/workflows/release-internal.yml b/.github/workflows/release-internal.yml new file mode 100644 index 000000000..89c042bcb --- /dev/null +++ b/.github/workflows/release-internal.yml @@ -0,0 +1,99 @@ +name: Release Internal + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + TERM: xterm-256color + FORCE_COLOR: 1 + +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 + + permissions: + contents: read + packages: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Java + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'adopt' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Decode mainnet release google-services.json + env: + MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64 }} + run: | + set -euo pipefail + test -n "$MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64" + mkdir -p app/src/mainnetRelease + printf '%s' "$MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > app/src/mainnetRelease/google-services.json + + - name: Decode internal keystore + env: + INTERNAL_KEYSTORE_BASE64: ${{ secrets.INTERNAL_KEYSTORE_BASE64 }} + run: | + set -euo pipefail + test -n "$INTERNAL_KEYSTORE_BASE64" + umask 077 + keystore_path="$RUNNER_TEMP/internal.keystore" + printf '%s' "$INTERNAL_KEYSTORE_BASE64" | base64 --decode > "$keystore_path" + echo "KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV" + + - name: Build internal release APK + env: + GPR_USER: ${{ secrets.GPR_USER || github.actor }} + GPR_TOKEN: ${{ secrets.GPR_TOKEN || github.token }} + GITHUB_TOKEN: ${{ secrets.GPR_TOKEN || github.token }} + KEYSTORE_PASSWORD: ${{ secrets.INTERNAL_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.INTERNAL_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.INTERNAL_KEY_PASSWORD }} + run: ./gradlew assembleMainnetRelease --no-daemon --stacktrace + + - name: Verify internal release signature + run: | + set -euo pipefail + android_sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + test -n "$android_sdk_root" + apksigner_path="$(find "$android_sdk_root/build-tools" -name apksigner -type f | sort -V | tail -n 1)" + test -n "$apksigner_path" + + apk_count=0 + while IFS= read -r -d '' apk_path; do + apk_count=$((apk_count + 1)) + "$apksigner_path" verify --verbose --print-certs "$apk_path" + done < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + test "$apk_count" -gt 0 + + - name: Collect internal artifacts + id: artifacts + run: | + 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) + echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" + + - name: Upload internal artifacts + uses: actions/upload-artifact@v6 + with: + name: bitkit-internal-release-${{ github.run_number }} + path: ${{ steps.artifacts.outputs.artifact_dir }} + retention-days: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..54c810537 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,123 @@ +name: Release + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + TERM: xterm-256color + FORCE_COLOR: 1 + +jobs: + build-release: + 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 + + permissions: + contents: read + packages: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Java + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'adopt' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Decode mainnet release google-services.json + env: + MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64 }} + run: | + set -euo pipefail + test -n "$MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64" + mkdir -p app/src/mainnetRelease + printf '%s' "$MAINNET_RELEASE_GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > app/src/mainnetRelease/google-services.json + + - name: Decode release keystore + env: + BITKIT_KEYSTORE_BASE64: ${{ secrets.BITKIT_KEYSTORE_BASE64 }} + run: | + set -euo pipefail + test -n "$BITKIT_KEYSTORE_BASE64" + umask 077 + keystore_path="$RUNNER_TEMP/bitkit.keystore" + printf '%s' "$BITKIT_KEYSTORE_BASE64" | base64 --decode > "$keystore_path" + echo "KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV" + + - name: Build release artifacts + env: + GPR_USER: ${{ secrets.GPR_USER || github.actor }} + GPR_TOKEN: ${{ secrets.GPR_TOKEN || github.token }} + GITHUB_TOKEN: ${{ secrets.GPR_TOKEN || github.token }} + KEYSTORE_PASSWORD: ${{ secrets.BITKIT_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.BITKIT_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.BITKIT_KEY_PASSWORD }} + run: ./gradlew assembleMainnetRelease bundleMainnetRelease --no-daemon --stacktrace + + - name: Verify release signatures + run: | + set -euo pipefail + android_sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + test -n "$android_sdk_root" + apksigner_path="$(find "$android_sdk_root/build-tools" -name apksigner -type f | sort -V | tail -n 1)" + test -n "$apksigner_path" + + apk_count=0 + while IFS= read -r -d '' apk_path; do + apk_count=$((apk_count + 1)) + "$apksigner_path" verify --verbose --print-certs "$apk_path" + done < <(find app/build/outputs/apk/mainnet/release -name 'bitkit-mainnet-release-*.apk' -print0) + test "$apk_count" -gt 0 + + bundle_count=0 + while IFS= read -r -d '' bundle_path; do + bundle_count=$((bundle_count + 1)) + verify_output="$(mktemp)" + if ! jarsigner -verify -verbose -certs "$bundle_path" 2>&1 | tee "$verify_output"; then + rm -f "$verify_output" + exit 1 + fi + if grep -qi "jar is unsigned" "$verify_output"; then + echo "Unsigned bundle: $bundle_path" + rm -f "$verify_output" + exit 1 + fi + if ! grep -qi "jar verified" "$verify_output"; then + echo "Bundle signature verification did not report success: $bundle_path" + rm -f "$verify_output" + exit 1 + fi + rm -f "$verify_output" + done < <(find app/build/outputs/bundle/mainnetRelease -name 'bitkit-mainnet-release-*.aab' -print0) + test "$bundle_count" -gt 0 + + - name: Collect release artifacts + id: artifacts + run: | + 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) + echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" + + - name: Upload release artifacts + uses: actions/upload-artifact@v6 + with: + name: bitkit-release-${{ github.run_number }} + path: ${{ steps.artifacts.outputs.artifact_dir }} + retention-days: 30 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index c6da4ed2e..935be2829 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -41,7 +41,14 @@ jobs: gradle-${{ runner.os }}- - name: Decode google-services.json - run: echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/google-services.json + run: | + set -euo pipefail + if [ -z "${GOOGLE_SERVICES_JSON_BASE64:-}" ]; then + echo "GOOGLE_SERVICES_JSON_BASE64 is empty; using checked-in placeholder." + exit 0 + fi + mkdir -p app/src/debug + printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 -d > app/src/debug/google-services.json env: GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} diff --git a/.gitignore b/.gitignore index 5eb837641..85acba5a5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ local.properties !*.local.template* # Secrets google-services.json +# Tracked fallback config used only so debug variants compile from a fresh clone. +!app/google-services.json .env .env.* !.env.example diff --git a/README.md b/README.md index 8f1fd45e7..899569250 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,15 @@ This repository contains the **native Android app** for Bitkit. #### 1. Firebase Configuration -Download `google-services.json` from the Firebase Console for each of the following build flavor groups,: -- dev/tnet/mainnetDebug: Place in `app/google-services.json` +Dev and testnet debug builds use a checked-in placeholder at `app/google-services.json` so a fresh clone can compile without private Firebase files. + +Download `google-services.json` from the Firebase Console when you need real Firebase integration for push notifications testing: +- Debug builds: Place in `app/src/debug/google-services.json` - mainnetRelease: Place in `app/src/mainnetRelease/google-services.json` -> **Note**: Each flavor requires its own Firebase project configuration. The mainnet flavor will fail to build without its dedicated `google-services.json` file. +The debug file above is ignored by Git and takes precedence over the checked-in placeholder. To use real Firebase integration across debug variants, make sure it includes each application ID you build. + +> **Note**: Placeholder config is only for local dev and testnet debug builds. FCM token registration and push notifications require real Firebase configuration. The mainnet release flavor should always use the real `mainnetRelease/google-services.json` file. #### 2. GitHub Packages setup diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 000000000..2526ef70c --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "000000000000", + "project_id": "bitkit-debug-placeholder", + "storage_bucket": "bitkit-debug-placeholder.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000", + "android_client_info": { + "package_name": "to.bitkit.dev" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDebugPlaceholderKeyForLocalBuilds000" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000002", + "android_client_info": { + "package_name": "to.bitkit.tnet" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDebugPlaceholderKeyForLocalBuilds002" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e3a1da778..9790eeb9e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,10 +12,12 @@ fun getGithubCredentials( passKey: String = "gpr.key", userKey: String = "gpr.user", ): Pair { - val user = System.getenv("GITHUB_ACTOR") + val user = System.getenv("GPR_USER") + ?: System.getenv("GITHUB_ACTOR") ?: providers.gradleProperty(userKey).orNull ?: localProperties.getProperty(userKey) - val key = System.getenv("GITHUB_TOKEN") + val key = System.getenv("GPR_TOKEN") + ?: System.getenv("GITHUB_TOKEN") ?: providers.gradleProperty(passKey).orNull ?: localProperties.getProperty(passKey) return user to key