diff --git a/.agents/commands/release.md b/.agents/commands/release.md index 7e56d29f8..532790c65 100644 --- a/.agents/commands/release.md +++ b/.agents/commands/release.md @@ -205,12 +205,13 @@ Print the path to the release notes file so the user can share it for review. ### 7. Build Mainnet Release ```bash -./gradlew assembleMainnetRelease +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` -Verify the file exists. If the build 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. ### 8. Upload APK to Draft Release diff --git a/.cursor/rules/rules.main.mdc b/.cursor/rules/rules.main.mdc index ff0409ac5..39f159d04 100644 --- a/.cursor/rules/rules.main.mdc +++ b/.cursor/rules/rules.main.mdc @@ -75,6 +75,6 @@ alwaysApply: true --- ## Rules for Android Unit tests and Instrumentation tests: -- run unit tests for specific files like this: `./gradlew :app:testDevDebugUnitTest --tests "to.bitkit.repositories.LightningRepoTest"` +- run unit tests for specific files like this: `just test file "to.bitkit.repositories.LightningRepoTest"` - write unit tests in the same style and using same libraries as: `CurrencyRepoTest`, `LightningRepoTest`, `WalletRepoTest` - in unit tests, use asserts from `kotlin.test` and mockito-kotlin for mocks diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..5af4ffb5d --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Copy to .env and uncomment only what you need. + +# GITHUB_ACTOR= +# GITHUB_TOKEN= +# TX_TOKEN= + +# KEYSTORE_FILE= +# KEYSTORE_PASSWORD= +# KEY_ALIAS= +# KEY_PASSWORD= + +# E2E_BACKEND=local +# E2E_HOMEGATE_URL=http://127.0.0.1:6288 + +# TREZOR_BRIDGE=true +# TREZOR_BRIDGE_URL=http://10.0.2.2:21325 diff --git a/.gitignore b/.gitignore index 1700753e3..5eb837641 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ local.properties # Secrets google-services.json .env +.env.* +!.env.example *.keystore !debug.keystore keystore.* diff --git a/AGENTS.md b/AGENTS.md index 3f34b1bdb..0788a782f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,43 +10,46 @@ Durable shared agent command specs live in `.agents/commands/`. For PR creation, ```sh # compile -./gradlew compileDevDebugKotlin +just compile + +# Build, install, launch dev app on connected target +just run # Build for dev -./gradlew assembleDevDebug +just build # Run unit tests -./gradlew testDevDebugUnitTest +just test # Run specific unit test file -./gradlew testDevDebugUnitTest --tests LightningRepoTest +just test file LightningRepoTest # Run instrumented tests -./gradlew connectedDevDebugAndroidTest +just test android # Build for E2E tests (UI hooks enabled, local Electrum by default) -E2E=true ./gradlew assembleDevRelease +just e2e # Build for E2E tests with geoblocking disabled -GEO=false E2E=true ./gradlew assembleDevRelease +just e2e no geo # Build for E2E tests using network Electrum (not local; staging/mainnet based on flavor) -E2E=true E2E_BACKEND=network ./gradlew assembleTnetRelease +just e2e network assembleTnetRelease # Lint using detekt -./gradlew detekt +just lint # Auto-format using detekt -./gradlew detekt --auto-correct +just format # Update detekt baseline -./gradlew detektBaseline +just lint baseline # Install dev build -./gradlew installDevDebug +just install # Clean build artifacts -./gradlew clean +just clean ``` ## Architecture Overview @@ -162,7 +165,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - USE coding rules from `.cursor/default.rules.mdc` - For multi-step changes, stacked PR surgery, and review follow-up with several small edits, batch validation instead of running the full build/check suite after every edit. Run the relevant Gradle checks once the coherent change set is ready, before updating a PR or pushing. -- Still run `./gradlew compileDevDebugKotlin`, `./gradlew testDevDebugUnitTest`, and `./gradlew detekt` before the final PR update/push for code changes, and fix failures before pushing. +- Still run `just compile`, `just test`, and `just lint` before the final PR update/push for code changes, and fix failures before pushing. - After fixing validation failures, rerun the narrowest useful check that proves the fix. If only test files changed, prefer the targeted test task and a test-focused lint/detekt check when the project tooling supports it; otherwise use the standard detekt task before pushing. - Use narrower checks earlier only when they answer an immediate risk, e.g. a single unit test after touching focused business logic or a Kotlin compile after a risky refactor. - ALWAYS ask clarifying questions to ensure an optimal plan when encountering functional or technical uncertainties in requests diff --git a/Justfile b/Justfile new file mode 100644 index 000000000..a0b861ea2 --- /dev/null +++ b/Justfile @@ -0,0 +1,164 @@ +set dotenv-load +set dotenv-filename := ".env" +set windows-shell := ["sh", "-cu"] + +gradle := "./gradlew" + +default: + @just list + +list: + @printf "%s\n" \ + "list" \ + "init" \ + "compile" \ + "run" \ + "build [TASK]" \ + "release" \ + "install" \ + "test" \ + "test file PATTERN" \ + "test android" \ + "test lane LANE" \ + "lint" \ + "lint baseline" \ + "format" \ + "translations pull" \ + "translations push source" \ + "translations push all" \ + "e2e [network|no geo|TASK] [TASK]" \ + "changelog [all|next|hotfix]" \ + "clean" + +init: + #!/usr/bin/env sh + set -eu + + if [ -e .env ]; then + echo ".env already exists" + exit 0 + fi + + cp .env.example .env + echo "Created .env" + +compile: + {{ gradle }} compileDevDebugKotlin + +run: + #!/usr/bin/env sh + set -eu + + app_id="to.bitkit.dev" + app_dir="app/build/outputs/apk/dev/debug" + + if ! command -v adb >/dev/null 2>&1; then + echo "adb is required to run the app." >&2 + exit 1 + fi + + if [ -n "${ANDROID_SERIAL:-}" ]; then + device_id="$ANDROID_SERIAL" + else + echo "Looking for connected Android devices..." + device_id="$( + adb devices -l \ + | awk 'NR > 1 && $2 == "device" && $1 !~ /^emulator-/ { print $1; exit }' + )" + + if [ -z "$device_id" ]; then + device_id="$( + adb devices -l \ + | awk 'NR > 1 && $2 == "device" { print $1; exit }' + )" + fi + fi + + if [ -z "$device_id" ]; then + echo "No connected Android device found." >&2 + exit 1 + fi + + device_name="$( + adb -s "$device_id" shell getprop ro.product.model 2>/dev/null \ + | tr -d '\r' \ + || true + )" + + if [ -z "$device_name" ]; then + device_name="$device_id" + fi + + echo "Using $device_name ($device_id)" + echo "Building Debug app..." + {{ gradle }} assembleDevDebug + + app_path="$( + find "$app_dir" -maxdepth 1 -name '*-universal.apk' -type f \ + | sort \ + | tail -n 1 + )" + + if [ -z "$app_path" ]; then + app_path="$( + find "$app_dir" -maxdepth 1 -name '*.apk' -type f \ + | sort \ + | tail -n 1 + )" + fi + + if [ -z "$app_path" ]; then + echo "No APK found in $app_dir." >&2 + exit 1 + fi + + echo "Installing $app_path..." + adb -s "$device_id" install -r "$app_path" + + echo "Launching $app_id..." + 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 + + pid="$( + adb -s "$device_id" shell pidof -s "$app_id" 2>/dev/null \ + | tr -d '\r' \ + || true + )" + + if [ -z "$pid" ]; then + echo "Launched $app_id" + exit 0 + fi + + echo "Streaming logs for $app_id (pid $pid). Press Ctrl-C to stop." + adb -s "$device_id" logcat --pid "$pid" + +build task="assembleDevDebug": + {{ gradle }} {{ task }} + +release: + {{ gradle }} assembleMainnetRelease bundleMainnetRelease + +install: + {{ gradle }} installDevDebug + +test target="" value="": + {{ if target == "" { gradle + " testDevDebugUnitTest" } else if target == "android" { gradle + " connectedDevDebugAndroidTest" } else if target == "file" { if value == "" { error("usage: just test file PATTERN") } else { gradle + " testDevDebugUnitTest --tests '" + value + "'" } } else if target == "lane" { if value == "" { error("usage: just test lane LANE") } else { gradle + " connectedDevDebug" + value + "AndroidTest" } } else { error("usage: just test [file PATTERN|android|lane LANE]") } }} + +lint target="": + {{ if target == "" { gradle + " detekt --rerun-tasks" } else if target == "baseline" { gradle + " detektBaseline --rerun-tasks" } else { error("usage: just lint [baseline]") } }} + +format: + {{ gradle }} detekt --auto-correct --rerun-tasks + +translations action value="": + {{ if action == "pull" { "./scripts/pull-translations.sh" } else if action == "push" { if value == "source" { "tx push --source" } else if value == "all" { "./scripts/push-translations.sh" } else { error("usage: just translations pull|push source|push all") } } else { error("usage: just translations pull|push source|push all") } }} + +e2e mode="" value="" task="assembleDevRelease": + {{ if mode == "" { "E2E=true " + gradle + " " + task } else if mode == "network" { "E2E=true E2E_BACKEND=network " + gradle + " " + if value == "" { task } else { value } } else if mode == "no" { if value == "geo" { "GEO=false E2E=true " + gradle + " " + task } else { error("usage: just e2e no geo [TASK]") } } else { "E2E=true " + gradle + " " + mode } }} + +changelog target="all": + ./scripts/preview-changelog.sh --target {{ target }} + +clean: + {{ gradle }} clean diff --git a/README.md b/README.md index 29c01e91d..8f1fd45e7 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,22 @@ See also: - For LNURL dev testing see [bitkit-docker](https://github.com/synonymdev/bitkit-docker) +### Command Launcher + +This repo includes a Justfile for common Gradle and script commands. + +Install `just` ([more options](https://github.com/casey/just#packages)): +- macOS: `brew install just` +- Linux: `mkdir -p ~/.local/bin && curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin` +- Windows: `winget install --id Casey.Just --exact` +- Windows shell: install Git for Windows or another `sh` provider for Bash-backed recipes. + +Set up local env: +- `just init` +- Uncomment only the values you need in the fresh `.env` file, such as `GITHUB_ACTOR`, `GITHUB_TOKEN`, `TX_TOKEN`, and E2E build settings. + +Run `just list` to see available commands. The common ones are `just init`, `just compile`, `just run`, `just build`, `just release`, `just test`, `just lint`, and `just translations pull`. `just run` prefers a physical device and falls back to an emulator. + ### Lint This project uses detekt with default ktlint and compose-rules for android code linting. @@ -57,8 +73,8 @@ The following IDE plugins are recommended for development with Android Studio or **Commands** ```sh -./gradlew detekt # run analysis + formatting check -./gradlew detekt --auto-correct # auto-fix formatting issues +just lint # run analysis + formatting check +just format # auto-fix formatting issues ``` Reports are generated in: `app/build/reports/detekt/`. @@ -78,11 +94,11 @@ Install or update all bundled plugins with: **Commands** ```sh -./gradlew testDevDebugUnitTest # run unit tests +just test # run unit tests # run android tests: -./gradlew installDevDebug # install -./gradlew connectedDevDebugAndroidTest # run +just install # install +just test android # run ``` ## Localization @@ -104,7 +120,7 @@ To pull the latest translations from Transifex: 3. **Pull translations**: ```sh - ./scripts/pull-translations.sh + just translations pull ``` ### Pushing Source Strings @@ -112,15 +128,17 @@ To pull the latest translations from Transifex: When you add or modify translation keys in the EN source file, push them to Transifex: ```sh -tx push --source +just translations push source ``` +To intentionally round-trip local source and translation files back to Transifex, use `just translations push all`. + ### Translation Workflow 1. **Add/modify strings** in `app/src/main/res/values/strings.xml` -2. **Push to Transifex:** `tx push --source` +2. **Push to Transifex:** `just translations push source` 3. **Translators** work on translations in Transifex -4. **Pull translations:** `./scripts/pull-translations.sh` +4. **Pull translations:** `just translations pull` 5. **Commit** the updated translation files ## Build @@ -143,8 +161,8 @@ Setup the signing config: Increment `versionCode` and `versionName` in `app/build.gradle.kts`, then run: ```sh -./gradlew assembleDevRelease -# ./gradlew assembleRelease # for all flavors +just build assembleDevRelease +# just build assembleRelease # for all flavors ``` APK is generated in `app/build/outputs/apk/_flavor_/release`. (`_flavor_` can be any of 'dev', 'mainnet', 'tnet'). @@ -155,25 +173,19 @@ Example for dev: `app/build/outputs/apk/dev/release` To build the mainnet flavor for release run: ```sh -./gradlew assembleMainnetRelease +just release ``` #### Android App Bundle (AAB) -For Play Store submission, build an AAB instead of APK: - -```sh -./gradlew bundleMainnetRelease -``` - -AAB is generated in `app/build/outputs/bundle/mainnetRelease/`. +`just release` builds both the mainnet APK and Play Store AAB. AAB is generated in `app/build/outputs/bundle/mainnetRelease/`. ### Build for E2E Testing Pass `E2E=true` and build any flavor. By default, E2E uses a local Electrum override. ```sh -E2E=true ./gradlew assembleDevRelease +just e2e ``` #### Use Network Electrum (Staging/Mainnet) @@ -182,11 +194,11 @@ Set `E2E_BACKEND=network` to use the network Electrum based on the build flavor: ```sh # regtest (dev flavor) -E2E=true E2E_BACKEND=network ./gradlew assembleDevRelease +just e2e network assembleDevRelease # testnet (tnet flavor) -E2E=true E2E_BACKEND=network ./gradlew assembleTnetRelease +just e2e network assembleTnetRelease # mainnet -E2E=true E2E_BACKEND=network ./gradlew assembleMainnetRelease +just e2e network assembleMainnetRelease ``` #### Disable Geoblocking Checks @@ -194,7 +206,7 @@ E2E=true E2E_BACKEND=network ./gradlew assembleMainnetRelease By default, geoblocking checks via API are enabled. To disable at build time, use the `GEO` environment variable: ```sh -GEO=false E2E=true ./gradlew assembleDevRelease +just e2e no geo ``` ## Contributing diff --git a/scripts/pull-translations.sh b/scripts/pull-translations.sh index 140c36968..a89cba258 100755 --- a/scripts/pull-translations.sh +++ b/scripts/pull-translations.sh @@ -69,11 +69,26 @@ rename_or_merge() { # Validate XML file is well-formed before processing validate_xml() { local file="$1" - # Basic validation: check for opening and closing resources tags + if command -v python3 &> /dev/null; then + python3 - "$file" <<'PY' +import sys +import xml.etree.ElementTree as ET + +try: + ET.parse(sys.argv[1]) +except Exception as e: + print(e) + raise SystemExit(1) +PY + return $? + fi + + # Fallback validation: check for opening and closing resources tags if ! grep -q '/dev/null || ! grep -q '' "$file" 2>/dev/null; then - echo " Warning: $file appears to be malformed XML, skipping normalization" + echo "missing resources root" return 1 fi + return 0 } @@ -164,10 +179,33 @@ echo "" echo "Normalizing XML formatting..." # Normalize XML +MARKUP_FIXED_COUNT=0 NORMALIZED_COUNT=0 +XML_ERROR_COUNT=0 while IFS= read -r file; do + if sed \ + -e 's# "$file.markup.tmp"; then + if cmp -s "$file" "$file.markup.tmp"; then + rm -f "$file.markup.tmp" + elif mv "$file.markup.tmp" "$file"; then + MARKUP_FIXED_COUNT=$((MARKUP_FIXED_COUNT + 1)) + else + echo " Warning: Failed to fix markup in $file" + rm -f "$file.markup.tmp" + fi + else + echo " Warning: Failed to scan markup in $file" + rm -f "$file.markup.tmp" + fi + # Validate XML before processing - if ! validate_xml "$file"; then + if ! validation_error=$(validate_xml "$file" 2>&1); then + echo " Error: $file is malformed XML: $validation_error" + XML_ERROR_COUNT=$((XML_ERROR_COUNT + 1)) continue fi @@ -196,8 +234,14 @@ while IFS= read -r file; do fi done < <(find "$RES_DIR" -type f -path "*/values-*/strings.xml" 2>/dev/null) +echo " Fixed markup in $MARKUP_FIXED_COUNT files" echo " Normalized $NORMALIZED_COUNT files" +if [ "$XML_ERROR_COUNT" -ne 0 ]; then + echo "Error: Found $XML_ERROR_COUNT malformed XML file(s) after pull" + exit 1 +fi + echo "" echo "Cleaning up empty files and directories..." @@ -239,3 +283,4 @@ echo "Complete!" echo " Renamed: $RENAMED_COUNT, Removed: $REMOVED_COUNT" echo " Normalized: $NORMALIZED_COUNT" echo " Deleted files: $EMPTY_COUNT, Deleted dirs: $DELETED_DIRS" +echo "Pull complete!"