Skip to content

fix(notify): ingest new usage events before checking alert thresholds… #1049

fix(notify): ingest new usage events before checking alert thresholds…

fix(notify): ingest new usage events before checking alert thresholds… #1049

Workflow file for this run

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