diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2659dad..a6564c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,14 @@ jobs: go-version-file: native/go.mod cache-dependency-path: native/go.sum + - name: Set up Ruby and Fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4.10" + bundler-cache: true + - name: Run native tests with the race detector - working-directory: native - run: go test -race ./... + run: bundle exec fastlane android native_tests android-tests: name: Android tests and checks @@ -53,6 +58,12 @@ jobs: go-version-file: native/go.mod cache-dependency-path: native/go.sum + - name: Set up Ruby and Fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4.10" + bundler-cache: true + - name: Install Android SDK components run: | sdkmanager="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" @@ -67,8 +78,42 @@ jobs: "ndk;29.0.14206865" echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> "$GITHUB_ENV" - - name: Build native library using the F-Droid recipe - run: ./scripts/build-fdroid-native.sh + - name: Run Android checks and verify the unsigned APK + run: bundle exec fastlane android android_checks - - name: Run Android tests, checks, and clean-store builds - run: ./gradlew --no-daemon testDebugUnitTest lintDebug assembleDebug assembleRelease + - name: Publish PR debug APK + if: github.event_name == 'pull_request' + id: debug_apk_upload + uses: actions/upload-artifact@v7 + with: + name: megaproxy-pr-${{ github.event.pull_request.number }}-debug-apk + path: app/build/outputs/apk/debug/app-debug.apk + if-no-files-found: error + compression-level: 0 + retention-days: 14 + + - name: Publish PR unsigned release APK + if: github.event_name == 'pull_request' + id: unsigned_release_apk_upload + uses: actions/upload-artifact@v7 + with: + name: megaproxy-pr-${{ github.event.pull_request.number }}-unsigned-release-apk + path: app/build/outputs/apk/release/app-release-unsigned.apk + if-no-files-found: error + compression-level: 0 + retention-days: 14 + + - name: Add APK download links to the job summary + if: github.event_name == 'pull_request' + env: + DEBUG_APK_URL: ${{ steps.debug_apk_upload.outputs.artifact-url }} + UNSIGNED_RELEASE_APK_URL: ${{ steps.unsigned_release_apk_upload.outputs.artifact-url }} + run: | + { + echo "## APK artifacts" + echo + echo "- [Download debug APK]($DEBUG_APK_URL) — signed with the disposable Android debug key" + echo "- [Download unsigned release APK]($UNSIGNED_RELEASE_APK_URL) — verified unsigned" + echo + echo "Artifacts expire after 14 days. Neither APK uses the MegaProxy release key." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/pr-artifacts-comment.yml b/.github/workflows/pr-artifacts-comment.yml new file mode 100644 index 0000000..7191515 --- /dev/null +++ b/.github/workflows/pr-artifacts-comment.yml @@ -0,0 +1,101 @@ +name: PR artifact links + +on: + workflow_run: + workflows: + - CI + types: + - completed + +permissions: + actions: read + contents: read + pull-requests: write + +jobs: + comment: + name: Add APK links to the pull request + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Create or update the APK artifact comment + uses: actions/github-script@v9 + with: + script: | + const run = context.payload.workflow_run; + const pullRequests = run.pull_requests || []; + + if (pullRequests.length !== 1) { + core.notice(`Expected one pull request for workflow run ${run.id}, found ${pullRequests.length}`); + return; + } + + const pullNumber = pullRequests[0].number; + const expectedArtifacts = { + debug: `megaproxy-pr-${pullNumber}-debug-apk`, + release: `megaproxy-pr-${pullNumber}-unsigned-release-apk`, + }; + const artifacts = await github.paginate( + github.rest.actions.listWorkflowRunArtifacts, + { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + per_page: 100, + } + ); + + const findArtifact = (name) => artifacts.find( + (artifact) => artifact.name === name && !artifact.expired + ); + const debugArtifact = findArtifact(expectedArtifacts.debug); + const releaseArtifact = findArtifact(expectedArtifacts.release); + + if (!debugArtifact || !releaseArtifact) { + core.setFailed(`Expected APK artifacts were not found for workflow run ${run.id}`); + return; + } + + const artifactUrl = (artifact) => + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + + `/actions/runs/${run.id}/artifacts/${artifact.id}`; + const marker = ''; + const body = [ + marker, + '## APK artifacts', + '', + `- [Download debug APK](${artifactUrl(debugArtifact)}) — signed with the disposable Android debug key`, + `- [Download unsigned release APK](${artifactUrl(releaseArtifact)}) — verified unsigned`, + '', + `Built from commit \`${run.head_sha.slice(0, 8)}\` by [CI run #${run.run_number}](${run.html_url}).`, + 'Artifacts expire after 14 days. Neither APK uses the MegaProxy release key.', + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullNumber, + per_page: 100, + }); + const previousComment = comments.find( + (comment) => comment.user.type === 'Bot' && comment.body.includes(marker) + ); + + if (previousComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: previousComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullNumber, + body, + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b24e3a..643fc2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,12 @@ jobs: go-version-file: native/go.mod cache-dependency-path: native/go.sum + - name: Set up Ruby and Fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4.10" + bundler-cache: true + - name: Install Android SDK components run: | sdkmanager="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" @@ -81,19 +87,12 @@ jobs: echo "MEGAPROXY_KEYSTORE_PATH=$keystore" >> "$GITHUB_ENV" echo "MEGAPROXY_KEY_PASSWORD_FILE=$password_file" >> "$GITHUB_ENV" - - name: Build optimized signed APKs - env: - MEGAPROXY_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - MEGAPROXY_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - MEGAPROXY_GIT_COMMIT: ${{ github.sha }} - run: ./scripts/build-release-apks.sh - - - name: Build signed App Bundle + - name: Build and verify signed release artifacts env: MEGAPROXY_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} MEGAPROXY_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} MEGAPROXY_GIT_COMMIT: ${{ github.sha }} - run: ./scripts/build-release-bundle.sh + run: bundle exec fastlane android release_artifacts - name: Publish GitHub Release env: diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..84d6c67 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.10 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2d0c53f..2bdf944 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -46,27 +46,21 @@ { "label": "Test: Go core", "type": "shell", - "command": "source \"$HOME/.zshrc.extra\"; go test ./...", - "options": { - "cwd": "${workspaceFolder}/native" - }, + "command": "source \"$HOME/.zshrc.extra\"; bundle exec fastlane android native_tests", "group": "test", "problemMatcher": [] }, { - "label": "Test: Android unit tests", + "label": "Test: Android checks", "type": "shell", - "command": "source \"$HOME/.zshrc.extra\"; ./gradlew testDebugUnitTest", + "command": "source \"$HOME/.zshrc.extra\"; bundle exec fastlane android android_checks", "group": "test", "problemMatcher": [] }, { "label": "Test: All", - "dependsOn": [ - "Test: Go core", - "Test: Android unit tests" - ], - "dependsOrder": "parallel", + "type": "shell", + "command": "source \"$HOME/.zshrc.extra\"; bundle exec fastlane android test", "group": { "kind": "test", "isDefault": true @@ -76,9 +70,7 @@ { "label": "Android: Build debug APK", "type": "shell", - "command": "source \"$HOME/.zshrc.extra\"; ./gradlew assembleDebug", - "dependsOn": ["Android: Build native AAR"], - "dependsOrder": "sequence", + "command": "source \"$HOME/.zshrc.extra\"; bundle exec fastlane android debug_artifact", "group": { "kind": "build", "isDefault": true diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..2b65e2b --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane", "2.238.0" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..bc01cfd --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,263 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1283.0) + aws-sdk-core (3.254.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.229.0) + aws-sdk-core (~> 3, >= 3.254.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.6) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20260902) + dotenv (2.8.1) + emoji_regex (3.2.3) + erb (6.0.7) + excon (1.7.1) + logger + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) + fastimage (2.4.1) + fastlane (2.238.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 2.0.0) + faraday (~> 2.7) + faraday-cookie_jar (~> 0.0.8) + faraday-follow_redirects (~> 0.3) + faraday-multipart (~> 1.0) + faraday-retry (~> 2.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + irb (>= 1.8) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 4) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.107.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (1.2.5) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.11) + representable (~> 3.0) + retriable (>= 3.1, < 5.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.66.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.4) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + io-console (0.9.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + net-http (0.9.1) + uri (>= 0.11.1) + nkf (0.3.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.7.0) + io-console (~> 0.5) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (4.2.0) + rexml (3.4.4) + rouge (3.28.0) + rubyzip (2.4.1) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + trailblazer-option (0.1.2) + tsort (0.2.0) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-25 + ruby + x86_64-linux + x86_64-linux-musl + +DEPENDENCIES + fastlane (= 2.238.0) + +BUNDLED WITH + 2.6.9 diff --git a/README.md b/README.md index 8d573f2..0ded559 100644 --- a/README.md +++ b/README.md @@ -227,20 +227,19 @@ export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865" export PATH="$JAVA_HOME/bin:$HOME/go/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH" ``` -Build the Go core and Android AAR: +Install Ruby 3.4 and the pinned Fastlane dependencies, then build and test the application through +the project's supported automation entry point: ```shell -cd native -go test ./... -gomobile bind -target=android -androidapi 26 -o ../app/libs/megaproxy.aar ./mobile -cd .. +bundle install +bundle exec fastlane android test ``` -Build and test the Android application: - -```shell -./gradlew testDebugUnitTest assembleDebug -``` +For a debug APK without the complete check suite, run +`bundle exec fastlane android debug_artifact`. The APK is written to +`app/build/outputs/apk/debug/app-debug.apk`. Gradle and the scripts under `scripts/` remain the +low-level implementation used by the Fastlane lanes. Installation details and the complete command +reference are in the [Fastlane workflow documentation](docs/en/fastlane.md) ([по-русски](docs/ru/fastlane.md)). Install the debug APK on a connected device: @@ -280,11 +279,10 @@ emulator, building, installing, viewing app-specific Logcat output, running test JDWP debugger. Select **Run MegaProxy on Emulator** for `Ctrl+F5` or **Attach MegaProxy (JDWP)** for `F5`. -Run all native and Android unit tests from VS Code with **Tasks: Run Test Task**, or from a shell: +Run all native and Android checks from VS Code with **Tasks: Run Test Task**, or from a shell: ```shell -(cd native && go test ./...) -./gradlew testDebugUnitTest +bundle exec fastlane android test ``` JDWP covers Kotlin and Java code only. Debug the Go core with its tests and privacy-safe diagnostic @@ -296,8 +294,7 @@ Build optimized and signed APKs for `arm64-v8a`, `armeabi-v7a`, `x86_64`, and `x universal APK used for reproducible F-Droid verification: ```shell -./scripts/build-release-apks.sh -./scripts/build-release-bundle.sh +bundle exec fastlane android release_artifacts ``` The scripts read the default signing key from `$HOME/AndroidApkKey` and its password from @@ -308,8 +305,9 @@ and `SHA256SUMS` are written to `dist/release`. The App Bundle contains every su stores generate and serve optimized device-specific APK splits from it. Go native symbols are provided as `mega-proxy-native-debug-symbols.zip` for upload in Play Console. -Pushing a version tag runs the GitHub release workflow, builds and verifies every APK and the App -Bundle, and attaches the artifacts to a GitHub Release. The tag must match `versionName` exactly: +Pushing a version tag runs the same Fastlane release lane in GitHub Actions, builds and verifies +every APK and the App Bundle, and attaches the artifacts to a GitHub Release. The tag must match +`versionName` exactly: ```shell git tag v0.0.4 diff --git a/artwork/launcher-icon.png b/artwork/launcher-icon.png deleted file mode 100644 index e402479..0000000 Binary files a/artwork/launcher-icon.png and /dev/null differ diff --git a/assets/branding/README.md b/assets/branding/README.md new file mode 100644 index 0000000..50ed9be --- /dev/null +++ b/assets/branding/README.md @@ -0,0 +1,10 @@ +# MegaProxy branding assets + +`megaproxy-launcher-master.png` is the current canonical raster artwork for the MegaProxy logo. +Use it as the visual source when regenerating derived application, store, or documentation assets. + +Derived assets live with the component that consumes them: + +- Android launcher resources: `app/src/main/res/mipmap-*` +- Google Play icon and graphics: `fastlane/metadata/android/*/images/` +- Repository and documentation graphics: `docs/assets/` diff --git a/docs/en/fastlane.md b/docs/en/fastlane.md new file mode 100644 index 0000000..8eb1645 --- /dev/null +++ b/docs/en/fastlane.md @@ -0,0 +1,75 @@ +# Fastlane workflow + +MegaProxy uses [Fastlane](https://fastlane.tools/) as the supported command-line entry point for +tests and build artifacts. Gradle, Go, and the scripts in `scripts/` remain the low-level build +implementation; Fastlane gives local development and CI the same named workflows. + +See the official [Fastlane Android setup guide](https://docs.fastlane.tools/getting-started/android/setup/) +and [Bundler setup instructions](https://docs.fastlane.tools/getting-started/android/setup/#use-a-gemfile) +for upstream installation details. The Gradle integration is documented in the +[Fastlane `gradle` action](https://docs.fastlane.tools/actions/gradle/). + +## Installation + +Install the project prerequisites listed in the root [README](../../README.md#building-from-source), +including JDK 21, Go, the Android SDK, and the Android NDK. Then install Ruby 3.4.10, as pinned in +`.ruby-version`. A Ruby version manager is recommended; do not depend on the old system Ruby +included with macOS. + +Install a current Bundler and the repository-pinned Fastlane dependency from the project root: + +```shell +gem install bundler +bundle install +``` + +Always run Fastlane through Bundler so that `Gemfile.lock` controls the exact dependency versions: + +```shell +bundle exec fastlane lanes +``` + +That command lists the lanes available in the checked-out version of the project. + +## Supported commands + +| Command | Result | +| --- | --- | +| `bundle exec fastlane android native_tests` | Runs all Go tests with the race detector. | +| `bundle exec fastlane android android_checks` | Builds the native AAR, runs Android unit tests and lint, builds a debug APK, then builds and verifies an unsigned release APK. It rejects any release-signing environment variables. | +| `bundle exec fastlane android test` | Runs `native_tests` and `android_checks`; this is the normal pre-commit command. | +| `bundle exec fastlane android debug_artifact` | Builds `app/build/outputs/apk/debug/app-debug.apk`. | +| `bundle exec fastlane android release_artifacts` | Builds and verifies the signed release APKs, AAB, native debug symbols, and `SHA256SUMS` in `dist/release`. | + +The release lane requires the signing configuration described in +[Signed release builds](../../README.md#signed-release-builds). It builds artifacts but does not +upload them to Google Play or publish a GitHub Release. GitHub Actions invokes the same lane and +handles GitHub Release publication separately. + +Pull-request CI runs `android_checks`, never `release_artifacts`. It receives no signing secrets, +rejects signing configuration if one is accidentally supplied, and verifies with Android +`apksigner` that `app-release-unsigned.apk` has no signature. The separately supported +`debug_artifact` lane produces an APK signed only with the standard disposable Android debug key; +it does not use the MegaProxy release identity. + +For pull requests, GitHub Actions uploads the debug and unsigned release APKs as two separately +named workflow artifacts. Direct download links are shown in the Android check's job summary, and +the files are retained for 14 days. After successful CI, a separate trusted `workflow_run` workflow +creates or updates one APK-links comment on the pull request. It does not check out, download, or +execute pull-request code or artifacts. These are test artifacts only: neither APK is signed with +the MegaProxy release key, and neither is published as a GitHub Release or sent to an app store. + +## Updating Fastlane + +Update Fastlane deliberately and commit both dependency files: + +```shell +bundle update fastlane +bundle exec fastlane lanes +bundle exec fastlane android test +``` + +Review changes to both `Gemfile` and `Gemfile.lock`. The official Fastlane documentation recommends +committing the lock file and using `bundle exec fastlane` locally and in CI. + +[Русская версия](../ru/fastlane.md) diff --git a/docs/en/index.md b/docs/en/index.md index 3e43402..666b99a 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -3,4 +3,8 @@ Server configurations and setup instructions have moved to the dedicated [MegaProxyServer repository](https://github.com/andre487/MegaProxyServer). +Developer documentation: + +- [Fastlane workflow](fastlane.md) + [Русская версия](../ru/index.md) diff --git a/docs/ru/fastlane.md b/docs/ru/fastlane.md new file mode 100644 index 0000000..14ca945 --- /dev/null +++ b/docs/ru/fastlane.md @@ -0,0 +1,77 @@ +# Работа с Fastlane + +MegaProxy использует [Fastlane](https://fastlane.tools/) как основной интерфейс командной строки +для тестов и сборочных артефактов. Gradle, Go и скрипты из `scripts/` остаются низкоуровневой +реализацией сборки, а Fastlane предоставляет одинаковые именованные сценарии для локальной работы +и CI. + +Официальные материалы: [установка Fastlane для Android](https://docs.fastlane.tools/getting-started/android/setup/), +[настройка через Bundler и Gemfile](https://docs.fastlane.tools/getting-started/android/setup/#use-a-gemfile) +и описание [Gradle action](https://docs.fastlane.tools/actions/gradle/). + +## Установка + +Сначала установите зависимости проекта из раздела +[Building from source](../../README.md#building-from-source): JDK 21, Go, Android SDK и Android NDK. +Затем установите Ruby 3.4.10 — эта версия зафиксирована в `.ruby-version`. Рекомендуется менеджер +версий Ruby; системный Ruby из macOS использовать не следует. + +Установите актуальный Bundler и зафиксированные проектом зависимости из корня репозитория: + +```shell +gem install bundler +bundle install +``` + +Fastlane всегда следует запускать через Bundler, чтобы использовались точные версии из +`Gemfile.lock`: + +```shell +bundle exec fastlane lanes +``` + +Эта команда показывает lanes, доступные в текущей версии проекта. + +## Поддерживаемые команды + +| Команда | Результат | +| --- | --- | +| `bundle exec fastlane android native_tests` | Запускает все Go-тесты с race detector. | +| `bundle exec fastlane android android_checks` | Собирает native AAR, запускает Android unit-тесты и lint, собирает debug APK, затем собирает и проверяет unsigned release APK. Команда отклоняет переменные release-подписи. | +| `bundle exec fastlane android test` | Выполняет `native_tests` и `android_checks`; основная команда перед коммитом. | +| `bundle exec fastlane android debug_artifact` | Собирает `app/build/outputs/apk/debug/app-debug.apk`. | +| `bundle exec fastlane android release_artifacts` | Собирает и проверяет подписанные APK, AAB, native debug symbols и `SHA256SUMS` в `dist/release`. | + +Для release lane нужна конфигурация подписи из раздела +[Signed release builds](../../README.md#signed-release-builds). Lane только собирает артефакты: он +не загружает их в Google Play и не публикует GitHub Release. GitHub Actions запускает тот же lane, +а публикацию GitHub Release выполняет отдельным шагом. + +В CI для pull request запускается `android_checks`, но никогда не `release_artifacts`. Job не +получает секреты подписи, отклоняет случайно переданную конфигурацию подписи и через Android +`apksigner` проверяет отсутствие подписи у `app-release-unsigned.apk`. Отдельный lane +`debug_artifact` создаёт APK, подписанный только стандартным одноразовым debug-ключом Android; ключ +официального релиза MegaProxy при этом не используется. + +Для pull request GitHub Actions загружает debug APK и unsigned release APK как два отдельных +workflow artifact с понятными именами. Прямые ссылки на скачивание выводятся в job summary проверки +Android, а сами файлы хранятся 14 дней. После успешного CI отдельный доверенный workflow с событием +`workflow_run` создаёт или обновляет один комментарий со ссылками в pull request. Он не делает +checkout, не скачивает и не исполняет код или артефакты из PR. Это только тестовые артефакты: ни +один из APK не подписан официальным release-ключом MegaProxy, не публикуется в GitHub Releases и не +отправляется в магазин приложений. + +## Обновление Fastlane + +Fastlane следует обновлять явно, после чего проверить и закоммитить оба файла зависимостей: + +```shell +bundle update fastlane +bundle exec fastlane lanes +bundle exec fastlane android test +``` + +Проверьте изменения в `Gemfile` и `Gemfile.lock`. Официальная документация Fastlane рекомендует +хранить lock-файл в репозитории и использовать `bundle exec fastlane` локально и в CI. + +[English version](../en/fastlane.md) diff --git a/docs/ru/index.md b/docs/ru/index.md index 8fb3681..0e267d0 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -3,4 +3,8 @@ Серверные конфигурации и инструкции по настройке перенесены в отдельный репозиторий [MegaProxyServer](https://github.com/andre487/MegaProxyServer). +Документация для разработчиков: + +- [Работа с Fastlane](fastlane.md) + [English version](../en/index.md) diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..04e016a --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,90 @@ +default_platform(:android) + +opt_out_usage +ensure_bundle_exec +ruby_version("3.4.0") + +project_root = File.expand_path("..", __dir__) + +platform :android do + desc "Run the native Go tests with the race detector" + lane :native_tests do + Dir.chdir(File.join(project_root, "native")) do + sh("go test -race ./...") + end + end + + desc "Build the native AAR used by Android builds" + private_lane :native_library do + sh(File.join(project_root, "scripts", "build-fdroid-native.sh")) + end + + desc "Run Android checks, build a debug APK, and verify that the CI release APK is unsigned" + lane :android_checks do + signing_variables = %w[ + MEGAPROXY_KEYSTORE_PATH + MEGAPROXY_KEYSTORE_PASSWORD + MEGAPROXY_KEY_ALIAS + MEGAPROXY_KEY_PASSWORD + MEGAPROXY_KEY_PASSWORD_FILE + ] + configured_signing_variables = signing_variables.select do |name| + !ENV.fetch(name, "").empty? + end + unless configured_signing_variables.empty? + UI.user_error!( + "Signing configuration must not be available to android_checks: " \ + "#{configured_signing_variables.join(', ')}" + ) + end + + native_library + gradle( + tasks: %w[testDebugUnitTest lintDebug assembleDebug assembleRelease], + flags: "--no-daemon", + project_dir: project_root + ) + + unsigned_apk = File.join( + project_root, + "app/build/outputs/apk/release/app-release-unsigned.apk" + ) + UI.user_error!("Expected unsigned release APK was not produced") unless File.file?(unsigned_apk) + + android_home = ENV.fetch("ANDROID_HOME") do + UI.user_error!("ANDROID_HOME is required to verify the release APK") + end + apksigner = File.join(android_home, "build-tools", "36.0.0", "apksigner") + UI.user_error!("Android apksigner is unavailable: #{apksigner}") unless File.executable?(apksigner) + + if system(apksigner, "verify", unsigned_apk, out: File::NULL, err: File::NULL) + UI.user_error!("CI release APK is unexpectedly signed: #{unsigned_apk}") + end + UI.success("Debug APK: app/build/outputs/apk/debug/app-debug.apk") + UI.success("Verified unsigned release APK: app/build/outputs/apk/release/app-release-unsigned.apk") + end + + desc "Run every native and Android test and check" + lane :test do + native_tests + android_checks + end + + desc "Build a debug APK after preparing the native library" + lane :debug_artifact do + native_library + gradle( + task: "assembleDebug", + flags: "--no-daemon", + project_dir: project_root + ) + UI.success("Debug APK: app/build/outputs/apk/debug/app-debug.apk") + end + + desc "Build and verify all signed release APK, AAB, symbols, and checksum artifacts" + lane :release_artifacts do + sh(File.join(project_root, "scripts", "build-release-apks.sh")) + sh(File.join(project_root, "scripts", "build-release-bundle.sh")) + UI.success("Release artifacts: dist/release") + end +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..f691648 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,16 @@ +# MegaProxy Fastlane lanes + +Fastlane is the supported entry point for tests and build artifacts. Install Ruby 3.4, then run +`bundle install` from the repository root. + +| Command | Purpose | +| --- | --- | +| `bundle exec fastlane android native_tests` | Run Go tests with the race detector | +| `bundle exec fastlane android android_checks` | Run Android tests and lint, build a debug APK and release APK, and prove that the release APK is unsigned | +| `bundle exec fastlane android test` | Run all native and Android checks | +| `bundle exec fastlane android debug_artifact` | Produce `app/build/outputs/apk/debug/app-debug.apk` | +| `bundle exec fastlane android release_artifacts` | Produce signed APKs, AAB, native symbols, and checksums in `dist/release` | + +The release lane deliberately delegates signing and artifact verification to the repository's +existing release scripts. It requires the signing environment documented in the root README. +Publishing to an app store is not performed by any lane.