Repository navigation
fix(watcher): guard AlarmReceiver finish() with FinishOnce #1047
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build | |
| on: | |
| push: | |
| branches: [ master ] | |
| tags: | |
| - v* | |
| pull_request: | |
| branches: [ master ] | |
| workflow_dispatch: | |
| # Allows manually triggering a build on any branch or tag. | |
| # Useful when RELEASE_PAT is missing and a tag push didn't auto-trigger this workflow. | |
| # Usage: gh workflow run build.yml --repo ActivityWatch/aw-android --ref refs/tags/v<version> | |
| env: | |
| # r28+: links native libs with 16 KB page alignment by default (Play requirement since Nov 2025) | |
| NDK_VERSION: '28.2.13676358' | |
| NODE_VERSION: '16' | |
| JAVA_VERSION: '17' | |
| # One run per workflow and ref. On PRs a new push cancels the superseded run. | |
| # On master the running build finishes, and queued intermediates collapse to | |
| # the newest commit, so a burst of merges doesn't queue a full build each. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| build-rust: | |
| name: Build aw-server-rust | |
| runs-on: ubicloud-standard-8 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| - name: Set RELEASE | |
| run: | | |
| echo "RELEASE=${{ startsWith(github.ref_name, 'v') }}" >> $GITHUB_ENV | |
| - name: Cache JNI libs | |
| uses: actions/cache@v4 | |
| id: cache-jniLibs | |
| env: | |
| cache-name: jniLibs | |
| with: | |
| path: mobile/src/main/jniLibs/ | |
| key: ${{ env.cache-name }}-release-${{ env.RELEASE }}-ndk-${{ env.NDK_VERSION }}-${{ hashFiles('.git/modules/aw-server-rust/HEAD') }} | |
| - name: Display structure of downloaded files | |
| if: steps.cache-jniLibs.outputs.cache-hit == 'true' | |
| run: | | |
| pushd mobile/src/main/jniLibs && ls -R && popd | |
| # Android SDK & NDK | |
| - name: Set up Android SDK | |
| if: steps.cache-jniLibs.outputs.cache-hit != 'true' | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| packages: platform-tools | |
| - name: Set up Android NDK | |
| if: steps.cache-jniLibs.outputs.cache-hit != 'true' | |
| run: | | |
| sdkmanager "ndk;${{ env.NDK_VERSION }}" | |
| ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }}" | |
| ls $ANDROID_NDK_HOME | |
| echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV | |
| # Rust | |
| - name: Set up Rust | |
| id: toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| if: steps.cache-jniLibs.outputs.cache-hit != 'true' | |
| - name: Set up Rust toolchain for Android NDK | |
| if: steps.cache-jniLibs.outputs.cache-hit != 'true' | |
| run: | | |
| ./aw-server-rust/install-ndk.sh | |
| - name: Cache cargo build | |
| uses: actions/cache@v4 | |
| if: steps.cache-jniLibs.outputs.cache-hit != 'true' | |
| env: | |
| cache-name: cargo-build-target | |
| with: | |
| path: aw-server-rust/target | |
| # key needs to contain cachekey due to https://github.com/ActivityWatch/aw-server-rust/issues/180 | |
| key: ${{ env.cache-name }}-${{ runner.os }}-release-${{ env.RELEASE }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ env.cache-name }}-${{ runner.os }}-release-${{ env.RELEASE }}-${{ steps.toolchain.outputs.cachekey }}- | |
| - name: Build aw-server-rust | |
| if: steps.cache-jniLibs.outputs.cache-hit != 'true' | |
| run: | | |
| make aw-server-rust | |
| - name: Check that jniLibs present and valid ELF | |
| run: | | |
| python3 scripts/check-jnilibs.py | |
| - name: Upload jniLibs artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: jniLibs | |
| path: mobile/src/main/jniLibs/ | |
| # This needs to be a separate job since it's used by both build-apk and release-fastlane. | |
| # versionCode is now committed to git by release.yml before the tag is pushed, | |
| # so we just read it from build.gradle — no Play Console query needed here. | |
| get-versionCode: | |
| name: Get latest versionCode | |
| runs-on: ubuntu-latest | |
| outputs: | |
| versionCode: ${{ steps.versionCode.outputs.versionCode }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| - name: Output versionCode | |
| id: versionCode | |
| run: | | |
| cat mobile/build.gradle | grep versionCode | sed 's/.*\s\([0-9]*\)$/versionCode=\1/' >> "$GITHUB_OUTPUT" | |
| build-apk: | |
| name: Build ${{ matrix.type }} | |
| runs-on: ubicloud-standard-4 | |
| needs: [build-rust, get-versionCode] | |
| # Skip on fork PRs: signing secrets are not available and upload would fail anyway. | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| type: ['apk', 'aab'] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| - uses: ActivityWatch/check-version-format-action@v2 | |
| id: version | |
| with: | |
| prefix: 'v' | |
| - name: Echo version | |
| run: | | |
| echo "${{ steps.version.outputs.full }} (stable: ${{ steps.version.outputs.is_stable }})" | |
| - name: Set RELEASE | |
| run: | | |
| # Build in release mode if on a tag/release (longer build times) | |
| echo "RELEASE=${{ startsWith(github.ref_name, 'v') }}" >> $GITHUB_ENV | |
| - name: Set up JDK | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| # Android SDK & NDK | |
| - name: Set up Android SDK | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| packages: platform-tools | |
| - name: Set up Android NDK | |
| run: | | |
| sdkmanager "ndk;${{ env.NDK_VERSION }}" | |
| ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }}" | |
| ls $ANDROID_NDK_HOME | |
| echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV | |
| # Restores jniLibs from cache | |
| # `actions/cache/restore` only restores, without saving back in a post-hook | |
| - uses: actions/cache/restore@v4 | |
| id: cache-jniLibs | |
| env: | |
| cache-name: jniLibs | |
| with: | |
| path: mobile/src/main/jniLibs/ | |
| key: ${{ env.cache-name }}-release-${{ env.RELEASE }}-ndk-${{ env.NDK_VERSION }}-${{ hashFiles('.git/modules/aw-server-rust/HEAD') }} | |
| fail-on-cache-miss: false | |
| - name: Check that jniLibs present and valid ELF | |
| run: | | |
| python3 scripts/check-jnilibs.py | |
| - name: Verify versionName matches tag | |
| if: startsWith(github.ref, 'refs/tags/v') # only on runs triggered from tag | |
| run: | | |
| # The -research suffix is a distribution-channel marker, not a version | |
| # component: a research build is the same code version, just published | |
| # to GitHub releases instead of Play. Normalize BOTH sides | |
| # (case-insensitively, matching assert-play-track.sh) so the assertion | |
| # passes whether the suffix came from the tag, the committed | |
| # versionName (e.g. the Release workflow run with a -research input, | |
| # which commits the suffixed versionName), or both. The | |
| # release-fastlane job separately fail-closes on any -research variant, | |
| # so a stripped match here can never reach Play. Strip logic and | |
| # coverage: scripts/strip-research-suffix.sh --self-test. | |
| # | |
| # The one exception that must NOT match: a committed versionName that | |
| # itself carries -research while the tag is plain. That combination | |
| # means the tree is a research-marked build (e.g. a plain tag created | |
| # on the Release workflow's research commit) and the resulting AAB | |
| # would reach Play production with a research-marked versionName — | |
| # so it fails closed here instead. | |
| bash scripts/strip-research-suffix.sh --self-test | |
| SHORT_VERSION="${GITHUB_REF_NAME#v}" | |
| SHORT_VERSION_RAW="$SHORT_VERSION" | |
| SHORT_VERSION=$(bash scripts/strip-research-suffix.sh "$SHORT_VERSION") | |
| COMMITTED_VERSION=$(sed -n -E "s/^[[:space:]]*versionName[[:space:]]+['\"]([^'\"]+)['\"].*/\\1/p" mobile/build.gradle) | |
| if [ -z "$COMMITTED_VERSION" ]; then | |
| echo "Failed to parse versionName from mobile/build.gradle." | |
| exit 1 | |
| fi | |
| case "$COMMITTED_VERSION" in | |
| *-[rR][eE][sS][eE][aA][rR][cC][hH]) | |
| case "$SHORT_VERSION_RAW" in | |
| *-[rR][eE][sS][eE][aA][rR][cC][hH]) : ;; # research tag on research-marked tree: fine | |
| *) | |
| echo "Refusing to build: committed versionName '$COMMITTED_VERSION' carries a -research suffix but the tag is a plain (non-research) tag." | |
| echo "A research-marked build must never be published from a plain tag (Play production upload)." | |
| exit 1 | |
| ;; | |
| esac | |
| ;; | |
| esac | |
| COMMITTED_VERSION=$(bash scripts/strip-research-suffix.sh "$COMMITTED_VERSION") | |
| if [ "$COMMITTED_VERSION" != "$SHORT_VERSION" ]; then | |
| echo "mobile/build.gradle versionName must match the release tag." | |
| echo "tag=$SHORT_VERSION committed=$COMMITTED_VERSION" | |
| exit 1 | |
| fi | |
| - name: Set versionCode | |
| run: | | |
| # Sets versionCode | |
| sed -i "s/versionCode .*/versionCode ${{needs.get-versionCode.outputs.versionCode}}/" \ | |
| mobile/build.gradle | |
| # Signing strategy is shared with gptme/gptme (.github/workflows/tauri.yml | |
| # release-android job + docs/contributing.rst "Android release signing" | |
| # there); signing itself happens in scripts/sign_apk.sh via the Makefile. | |
| # Keep the two implementations consistent when changing either. | |
| # gptme delivers the keystore as a base64 secret instead of age-encrypted | |
| # in-repo, pins the signer cert SHA-256, and fails closed on missing | |
| # secrets — planned to be adopted here too (see issue tracker). | |
| - uses: adnsio/setup-age-action@v1.2.0 | |
| - name: Load Android secrets | |
| if: env.KEY_ANDROID_JKS != null | |
| env: | |
| KEY_ANDROID_JKS: ${{ secrets.KEY_ANDROID_JKS }} | |
| run: | | |
| printf "$KEY_ANDROID_JKS" > android.jks.key | |
| cat android.jks.age | age -d -i android.jks.key -o android.jks | |
| rm android.jks.key | |
| - name: Assemble | |
| env: | |
| JKS_STOREPASS: ${{ secrets.KEY_ANDROID_JKS_STOREPASS }} | |
| JKS_KEYPASS: ${{ secrets.KEY_ANDROID_JKS_KEYPASS }} | |
| run: | | |
| if [ "${{ matrix.type }}" = apk ]; then | |
| # Per-ABI APKs first: signing dist/aw-android.apk moves the Gradle | |
| # output away, and build-apk-per-abi reads that same file (aw-android#61). | |
| make build-apk-per-abi dist/aw-android.apk | |
| else | |
| make dist/aw-android.${{ matrix.type }} | |
| fi | |
| - name: Upload | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: aw-android-${{ matrix.type }} | |
| path: dist/aw-android*.${{ matrix.type }} | |
| # Release builds run R8 (mobile/build.gradle `minifyEnabled true`), so | |
| # crash stacks are obfuscated without the mapping file. The AAB carries its | |
| # own copy for Play (BUNDLE-METADATA/com.android.tools.build.obfuscation/ | |
| # proguard.map), but nothing deobfuscates a stack pasted into an issue from | |
| # a sideloaded APK — so publish the mapping with the release too. | |
| # Only release builds produce one: the Makefile builds `debug` unless | |
| # RELEASE is true (i.e. on tags). | |
| - name: Collect R8 mapping | |
| if: env.RELEASE == 'true' | |
| run: | | |
| # Flavors move the mapping dir to per-variant `standardRelease`. | |
| cp mobile/build/outputs/mapping/standardRelease/mapping.txt \ | |
| dist/aw-android-mapping-${{ matrix.type }}.txt | |
| - name: Upload R8 mapping | |
| if: env.RELEASE == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: aw-android-mapping-${{ matrix.type }} | |
| path: dist/aw-android-mapping-${{ matrix.type }}.txt | |
| if-no-files-found: error | |
| test: | |
| name: Test | |
| runs-on: ubuntu-24.04 | |
| needs: [build-rust] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| # Cheap, no SDK: keeps the Play-track contract from regressing on every PR. | |
| # The leftover job-level `SUPPLY_TRACK: production` here never reached | |
| # fastlane (release-fastlane sets its own) and was a copy-paste footgun. | |
| # | |
| # strip-research-suffix.sh is also self-tested inside the tag-gated | |
| # versionName assertion, but that step only runs on tag builds — so a | |
| # regression in the helper would merge unnoticed and first surface on a | |
| # release tag. Exercise it here too, on every PR. | |
| # | |
| # user-facing-changelog.py only runs inside the tag-gated Release workflow, | |
| # so its filter needs the same every-PR coverage: a regression there would | |
| # first surface as a bad Play Store changelog on a release. | |
| # | |
| # check-production-rollout.py likewise only runs in Release; its decision | |
| # logic (which Play track states block a stable release) is tested here. | |
| - name: Assert Play-track resolver | |
| run: | | |
| bash scripts/assert-play-track.sh --self-test | |
| bash scripts/previous-release-tag.sh --self-test | |
| bash scripts/strip-research-suffix.sh --self-test | |
| python3 scripts/user-facing-changelog.py --self-test | |
| python3 scripts/check-production-rollout.py --self-test | |
| - name: Set up JDK | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| # Android SDK & NDK (NDK required at Gradle configuration time even for JVM unit tests) | |
| - name: Set up Android SDK | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| packages: platform-tools | |
| - name: Set up Android NDK | |
| run: | | |
| sdkmanager "ndk;${{ env.NDK_VERSION }}" | |
| ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }}" | |
| echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV | |
| - name: Download jniLibs artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: jniLibs | |
| path: mobile/src/main/jniLibs/ | |
| - name: Check that jniLibs present and valid ELF | |
| run: | | |
| python3 scripts/check-jnilibs.py | |
| # Test | |
| - name: Test | |
| run: | | |
| make test | |
| # R8 runs only for the release build type (#268), and the Makefile builds | |
| # `debug` unless RELEASE is set — so a broken keep rule or a missing-class | |
| # error would otherwise first surface on a release tag. This build is | |
| # unsigned and is not published; it only has to get through | |
| # minifyReleaseWithR8 and shrinkReleaseRes. lintVitalRelease is excluded: | |
| # the tag build still runs it, and it is not what this step is checking. | |
| - name: Build release variant (exercises R8) | |
| run: | | |
| # Pin the standard flavor explicitly (flavor task names are variant-suffixed). | |
| ./gradlew :mobile:assembleStandardRelease -x lintVitalStandardRelease | |
| test -s mobile/build/outputs/mapping/standardRelease/mapping.txt | |
| test-e2e: | |
| name: Test E2E | |
| needs: [build-rust] | |
| #if: false # disabled | |
| #runs-on: "macos-12" # macOS-latest | |
| runs-on: ubicloud-standard-8 | |
| env: | |
| MATRIX_E_SDK: ${{ matrix.api-level }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| api-level: [29] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| - name: Set RELEASE | |
| run: | | |
| echo "RELEASE=${{ startsWith(github.ref_name, 'v') }}" >> $GITHUB_ENV | |
| - name: Download jniLibs artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: jniLibs | |
| path: mobile/src/main/jniLibs/ | |
| - name: Display structure of downloaded files | |
| run: | | |
| pushd mobile/src/main/jniLibs && ls -R && popd | |
| - name: Set up JDK | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| - name: Set up Android SDK | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| packages: platform-tools | |
| - name: Set up Android NDK | |
| run: | | |
| sdkmanager "ndk;${{ env.NDK_VERSION }}" | |
| ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }}" | |
| ls $ANDROID_NDK_HOME | |
| echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> $GITHUB_ENV | |
| # The runner exposes /dev/kvm but the runner user is not in the kvm group, | |
| # so the emulator falls back to TCG software emulation ("ProbeKVM: This | |
| # user doesn't have permissions to use KVM"). Under TCG the emulator | |
| # intermittently goes `adb: device offline` after boot. Grant access so | |
| # android-emulator-runner's `disable-linux-hw-accel: auto` picks KVM. | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| sudo udevadm settle | |
| ls -l /dev/kvm | |
| - name: Run Emulator and E2E tests | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| timeout-minutes: 30 | |
| id: test | |
| with: | |
| api-level: ${{ matrix.api-level }} | |
| target: default | |
| arch: x86_64 | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot-save -memory 2048 | |
| disable-animations: true | |
| script: >- | |
| bash -o pipefail -c 'set +e; make test-e2e; test_status=$?; | |
| if [ -f mobile/build/outputs/apk/standard/debug/mobile-standard-debug.apk ]; then | |
| adb install -r mobile/build/outputs/apk/standard/debug/mobile-standard-debug.apk || true; | |
| adb shell monkey -p net.activitywatch.android.debug -c android.intent.category.LAUNCHER 1 || true; | |
| sleep 3; fi; | |
| adb shell screencap -p /sdcard/screenshot.png || true; | |
| adb pull /sdcard/screenshot.png . || true; | |
| mkdir -p mobile/build; | |
| adb logcat -d > mobile/build/logcat.log || true; | |
| exit "$test_status"' | |
| - name: Upload screenshot | |
| uses: actions/upload-artifact@v4 | |
| if: ${{ always() }} | |
| with: | |
| name: emulator-screenshot | |
| path: screenshot.png | |
| if-no-files-found: ignore | |
| - name: Upload logcat | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: logcat | |
| path: mobile/build/logcat.log | |
| if-no-files-found: ignore | |
| #- name: Publish Test Report | |
| # # # uses: mikepenz/action-junit-report@v3 | |
| # # # # # TODO: Format a little ? or Use some utility to confier GITHUB_STE_SUMMARY.html below into markdown before outputting it into $GITHUB_STEP_SUMMARY? | |
| # if: ${{ success() || steps.test.conclusion == 'failure'}} | |
| # # # with: | |
| # # # report_paths: '**/build/reports/*Tests/**/*.html' # '**/build/test-results/test/TEST-*.xml' | |
| # run: | | |
| # for file in ./mobile/build/reports/androidTests/connected/*.html; do cat $file >> GITHUB_STEP_SUMMARY.html ; done | |
| # # echo '<style>' >> GITHUB_STEP_SUMMARY.html;for file in ./mobile/build/reports/androidTests/connected/**/*.css; do cat $file >> GITHUB_STEP_SUMMARY.html ; done; echo '</style>' >> GITHUB_STEP_SUMMARY.html; | |
| # cat GITHUB_STEP_SUMMARY.html >> $GITHUB_STEP_SUMMARY | |
| # Runs the R8-minified release variant on an emulator (aw-android#268). | |
| # test-e2e only installs the debug APK, so R8-introduced runtime failures | |
| # (stripped JNI symbols, @JavascriptInterface methods, reflective classes) | |
| # would otherwise first surface on a release tag. | |
| test-release-smoke: | |
| name: Test release variant (R8 smoke) | |
| needs: [build-rust] | |
| runs-on: ubicloud-standard-8 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| - name: Download jniLibs artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: jniLibs | |
| path: mobile/src/main/jniLibs/ | |
| - name: Set up JDK | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: ${{ env.JAVA_VERSION }} | |
| - name: Set up Android SDK | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| packages: platform-tools | |
| - name: Set up Android NDK | |
| run: | | |
| sdkmanager "ndk;${{ env.NDK_VERSION }}" | |
| echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/${{ env.NDK_VERSION }}" >> $GITHUB_ENV | |
| - name: Build release variant (R8) | |
| run: ./gradlew :mobile:assembleStandardRelease -x lintVitalStandardRelease | |
| # Same as test-e2e: without this the emulator runs under TCG. | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| sudo udevadm settle | |
| ls -l /dev/kvm | |
| - name: Run emulator and smoke the release variant | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| # The action's own post-boot adb calls (unlock / screen-off timeout / | |
| # disabling animations) can wedge on this software-emulated runner (no | |
| # KVM, TCG) and never reach the script at all — on 1fd93de the step sat | |
| # silent for ~28min until this cap fired. Boot is ~2min and the smoke | |
| # ~3min, so a tighter cap fails a wedged emulator fast instead of | |
| # burning half an hour of runner time. | |
| timeout-minutes: 15 | |
| with: | |
| api-level: 29 | |
| target: default | |
| arch: x86_64 | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot-save | |
| disable-animations: true | |
| script: bash scripts/smoke-release-variant.sh mobile/build/outputs/apk/standard/release/mobile-standard-release-unsigned.apk | |
| - name: Upload logcat | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-smoke-logcat | |
| path: | | |
| mobile/build/release-smoke-logcat.log | |
| mobile/build/release-smoke-logcat-ui.xml | |
| if-no-files-found: ignore | |
| release-fastlane: | |
| needs: [build-apk, test, get-versionCode, test-release-smoke] | |
| if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-research') # skip research builds (GitHub release only) | |
| runs-on: ubuntu-latest | |
| # One Play publish at a time, across all tags: the workflow-level group is | |
| # per-ref, so two tag builds could otherwise each pass the rollout check | |
| # below and then race their uploads. Never cancel an in-flight publish. | |
| # (GitHub keeps one pending job per group, so a third concurrent tag build | |
| # would cancel the queued one; it shows as cancelled, never as published.) | |
| concurrency: | |
| group: play-publish | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download APK & AAB | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: aw-android-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Display structure of downloaded files | |
| working-directory: dist | |
| run: ls -R | |
| # Ruby & Fastlane | |
| # version set by .ruby-version | |
| - name: Set up Ruby and install fastlane | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| bundler-cache: true | |
| # detect if version tag is stable/beta | |
| - uses: nowsprinting/check-version-format-action@v4 | |
| id: version | |
| with: | |
| prefix: 'v' | |
| - name: Set SUPPLY_TRACK | |
| run: | | |
| # Set SUPPLY_TRACK based on whether this is a stable (x.y.z) or pre-release tag. | |
| # NOTE: nowsprinting/check-version-format-action classifies 0.x.x as not stable | |
| # (semver convention), but aw-android uses 0.x.x for production releases. | |
| # Check for pre-release suffixes directly instead (scripts/assert-play-track.sh). | |
| TAG="${{ github.ref_name }}" | |
| SUPPLY_TRACK="$(bash scripts/assert-play-track.sh resolve "$TAG")" | |
| # Fail closed: the job-level !contains('-research') condition is | |
| # case-sensitive (GitHub Actions has no case-insensitive contains), so | |
| # a variant like v1.2.3-Research would reach this job. Research builds | |
| # are GitHub-release only — refuse to publish them to Play here. | |
| if [ "$SUPPLY_TRACK" = none ]; then | |
| echo "error: ${TAG} is a research tag; Play publish is forbidden (GitHub release only)" >&2 | |
| exit 1 | |
| fi | |
| echo "SUPPLY_TRACK=${SUPPLY_TRACK}" >> "$GITHUB_ENV" | |
| { | |
| echo "## Play Store publish" | |
| echo "- tag: \`${TAG}\`" | |
| echo "- SUPPLY_TRACK: \`${SUPPLY_TRACK}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Assert Play track matches tag | |
| run: bash scripts/assert-play-track.sh assert "${{ github.ref_name }}" "${SUPPLY_TRACK}" | |
| - name: Set SUPPLY_ROLLOUT | |
| run: | | |
| # Production releases start as a staged rollout. The Release workflow | |
| # records the chosen fraction in the tag annotation; a tag pushed by hand | |
| # gets the 10% default. Widen with the Rollout workflow. | |
| TAG="${{ github.ref_name }}" | |
| git fetch --force --no-tags origin "refs/tags/${TAG}:refs/tags/${TAG}" >/dev/null 2>&1 || true | |
| SUPPLY_ROLLOUT="$(bash scripts/assert-play-track.sh rollout "$TAG" "$SUPPLY_TRACK")" | |
| echo "SUPPLY_ROLLOUT=${SUPPLY_ROLLOUT}" >> "$GITHUB_ENV" | |
| echo "- SUPPLY_ROLLOUT: \`${SUPPLY_ROLLOUT:-n/a (not production)}\`" >> "$GITHUB_STEP_SUMMARY" | |
| - uses: adnsio/setup-age-action@v1.2.0 | |
| - name: Load Android secrets | |
| env: | |
| KEY_FASTLANE_API: ${{ secrets.KEY_FASTLANE_API }} | |
| run: | | |
| printf "$KEY_FASTLANE_API" > fastlane/api-8546008605074111507-287154-450dc77b365f.json.key | |
| cat fastlane/api-8546008605074111507-287154-450dc77b365f.json.age \ | |
| | age -d -i fastlane/api-8546008605074111507-287154-450dc77b365f.json.key \ | |
| -o fastlane/api-8546008605074111507-287154-450dc77b365f.json | |
| rm fastlane/api-8546008605074111507-287154-450dc77b365f.json.key | |
| - name: Set up Python | |
| if: env.SUPPLY_TRACK == 'production' | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| # Same guard as the Release workflow, at the upload boundary: a hand-pushed | |
| # tag never ran Release, and an earlier tag build may have started a staged | |
| # rollout since Release checked. Play keeps one in-progress release per | |
| # track, so uploading now would silently replace it. Fails closed. The | |
| # override is the `allow_inprogress_rollout=true` line Release writes into | |
| # the tag annotation. This build's own versionCode doesn't block (re-runs). | |
| - name: Refuse to supersede an in-progress production rollout | |
| if: env.SUPPLY_TRACK == 'production' | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| VERSION_CODE: ${{ needs.get-versionCode.outputs.versionCode }} | |
| run: | | |
| ARGS=(--own-version-code "$VERSION_CODE") | |
| if git for-each-ref "refs/tags/${TAG}" --format='%(contents)' \ | |
| | grep -qx 'allow_inprogress_rollout=true'; then | |
| echo "::warning::Tag ${TAG} carries allow_inprogress_rollout=true: a staged production rollout in progress (or an unreadable Play track) will not block this upload." | |
| ARGS+=(--allow-inprogress) | |
| fi | |
| pip install --quiet google-auth requests || echo "::warning::pip install google-auth requests failed" | |
| python3 scripts/check-production-rollout.py \ | |
| --credentials fastlane/api-8546008605074111507-287154-450dc77b365f.json \ | |
| "${ARGS[@]}" | |
| - name: Release with fastlane | |
| run: | | |
| # Newer fastlane versions fail hard if rubygems.org isn't in the system gem sources list. | |
| # This is idempotent (exits 0 if already present). | |
| gem sources --add https://rubygems.org | |
| # Re-check at the publish boundary, then pass the asserted track on | |
| # argv. supply's default track is production; SUPPLY_TRACK via env is | |
| # the historical path, but an explicit --track is the fail-closed one. | |
| bash scripts/assert-play-track.sh assert "${{ github.ref_name }}" "${SUPPLY_TRACK}" | |
| # bundle exec fastlane supply run --apk dist/*.apk | |
| # Store-listing images are not re-uploaded on release: supply would | |
| # resend fastlane/metadata/**/images/icon.png every time, and Play | |
| # records that as an app-icon change that needs listing review on every | |
| # release. Change listing images deliberately, via the Play Console or | |
| # a one-off `fastlane supply run --skip_upload_aab`. | |
| if [ -n "$SUPPLY_ROLLOUT" ]; then | |
| bundle exec fastlane supply run --aab dist/*.aab --track "$SUPPLY_TRACK" --rollout "$SUPPLY_ROLLOUT" \ | |
| --skip_upload_images --skip_upload_screenshots | |
| else | |
| bundle exec fastlane supply run --aab dist/*.aab --track "$SUPPLY_TRACK" \ | |
| --skip_upload_images --skip_upload_screenshots | |
| fi | |
| release-gh: | |
| needs: [build-apk, test, test-release-smoke] | |
| if: startsWith(github.ref, 'refs/tags/v') # only on runs triggered from tag | |
| runs-on: ubuntu-latest | |
| steps: | |
| # Needed for changelog generation: full history + tags, and the submodule | |
| # pointers build_changelog.py summarizes (aw-server-rust). | |
| # Must run before download-artifact, since checkout cleans the workspace. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| submodules: recursive | |
| # build_changelog.py is maintained in ActivityWatch/activitywatch and is | |
| # explicitly designed to be reused by other repos (see its module docstring). | |
| # Check it out rather than vendoring a copy that would drift. | |
| # Pinned to a known-good commit (2026-08-24) to avoid executing unpinned | |
| # upstream scripts inside a release-capable job. | |
| - name: Checkout changelog tooling | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ActivityWatch/activitywatch | |
| ref: a17fa8aff7465268973a10d7d43aeb6741492e03 | |
| path: .changelog-tools | |
| sparse-checkout: scripts | |
| fetch-depth: 1 | |
| # Will download all artifacts to path | |
| - name: Download release APK & AAB | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: aw-android-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Display structure of downloaded files | |
| working-directory: dist | |
| run: ls -R | |
| # Classify the tag with the same rule as Play-track routing | |
| # (scripts/assert-play-track.sh): X.Y.Z → stable, anything else → pre-release. | |
| # Do not use check-version-format-action here: it treats 0.x.x as unstable | |
| # per semver, but aw-android ships 0.x.x as production. | |
| - name: Classify tag | |
| id: tag | |
| run: | | |
| # Use GITHUB_REF_NAME (already an env var) instead of interpolating | |
| # github.ref_name into the script. Tag names can contain shell | |
| # metacharacters; expression expansion happens before Bash parses. | |
| TRACK="$(bash scripts/assert-play-track.sh resolve "$GITHUB_REF_NAME")" | |
| if [[ "$TRACK" == production ]]; then | |
| IS_STABLE=true | |
| else | |
| IS_STABLE=false | |
| fi | |
| echo "is_stable=${IS_STABLE}" >> "$GITHUB_OUTPUT" | |
| echo "Classified ${GITHUB_REF_NAME}: track=${TRACK} is_stable=${IS_STABLE}" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install changelog deps | |
| run: pip install requests | |
| - name: Generate release notes | |
| run: | | |
| # get_latest_release.sh excludes the tag being released, so this | |
| # resolves to the previous release. STABLE_ONLY makes a stable release | |
| # diff against the last stable one rather than the last beta. | |
| IS_STABLE="${{ steps.tag.outputs.is_stable }}" | |
| LAST_RELEASE=$(STABLE_ONLY=${IS_STABLE} \ | |
| .changelog-tools/scripts/get_latest_release.sh) | |
| if [ -z "$LAST_RELEASE" ]; then | |
| echo "No previous release tag found; falling back to nearest ancestor tag." | |
| LAST_RELEASE=$(git describe --tags --abbrev=0 \ | |
| --exclude "$GITHUB_REF_NAME" "$GITHUB_REF_NAME") | |
| fi | |
| echo "Generating changelog for $LAST_RELEASE...$GITHUB_REF_NAME" | |
| python3 .changelog-tools/scripts/build_changelog.py \ | |
| --org ActivityWatch --repo aw-android \ | |
| --project-title "ActivityWatch for Android" \ | |
| --range "$LAST_RELEASE...$GITHUB_REF_NAME" \ | |
| --output release_notes.md | |
| cat release_notes.md | |
| # v0.14.0b2 shipped with an empty changelog because nothing generated | |
| # release_notes.md. Fail loudly here rather than publishing a release | |
| # whose body is silently blank. | |
| if [ ! -s release_notes.md ]; then | |
| echo "::error::release_notes.md is empty — refusing to publish a release with no changelog" | |
| exit 1 | |
| fi | |
| # create a release | |
| - name: Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| # Never create drafts. v0.14.0b2's Play Store "full changelog" link | |
| # pointed at a page non-admins could not see, and v0.14.0 sat as a | |
| # draft until it was published by hand (2026-09-13). Empty bodies | |
| # are already fail-closed above. Pre-releases are marked prerelease | |
| # so they do not steal "Latest release". | |
| draft: false | |
| # Compare to the string 'true': GHA boolean outputs are strings, and | |
| # "false" is truthy because it is non-empty. | |
| # https://github.com/actions/runner/issues/1483#issuecomment-994986996 | |
| prerelease: ${{ steps.tag.outputs.is_stable != 'true' }} | |
| # aw-android-mapping-{apk,aab}.txt are the R8 mapping files for the | |
| # two artifacts above; retrace an obfuscated stack from a sideloaded | |
| # build with them. Play gets its copy from inside the AAB. | |
| files: | | |
| dist/*.apk | |
| dist/*.aab | |
| dist/aw-android-mapping-*.txt | |
| body_path: release_notes.md |