mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
Fold per-push Android dev distribution into build-android.yml (which already builds a signed APK on every master push) instead of a second workflow that duplicated the whole build. On non-tag master pushes it stamps a dev versionCode into the working copy of build.gradle and, after the existing build, ships the play APK to the Play 'internal' track so a fresh build auto-updates on the phone. Internal testing skips review, so builds land in minutes; dev builds (code base+1..999) and releases (base+9000) share the track but stay distinct by versionCode. All new steps are gated to refs/heads/master (non-tag); the tag/release path is untouched, and every dev step is continue-on-error so it can never break the release-critical build. versionCode = last stable release's code + commits-since-that-tag (first-parent), staying in the 999-slot band above the release and below the next version's base. The base is derived from the last stable tag (not the working tree), so a version bump landing on master before its tag is pushed can't make dev codes jump ahead and then regress. Logic lives in tools/android-dev-version-code.js (reusing getAndroidVersionInfo) with unit tests; it degrades to skip on any anomaly. Reuses existing keystore/Play/Unsplash secrets; no new secrets.
199 lines
8.8 KiB
YAML
199 lines
8.8 KiB
YAML
name: Create android Play Store APK
|
|
on:
|
|
push:
|
|
branches: [master, release/*, test/git-actions]
|
|
tags:
|
|
- v*
|
|
workflow_dispatch:
|
|
inputs: {}
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
# Per-ref so rapid non-tag master pushes supersede each other (newest dev build
|
|
# wins). Groups are keyed by ref, so a master push can never cancel a `v*` tag
|
|
# release build (different ref = different group).
|
|
concurrency:
|
|
group: build-android-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
build-android:
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
|
|
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
|
steps:
|
|
- name: Checkout sources
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
|
|
with:
|
|
# Full commit/tag graph (blobless, so cheap) is needed to derive the
|
|
# per-push dev versionCode from the last stable tag.
|
|
fetch-depth: 0
|
|
filter: blob:none
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: 22
|
|
- name: Setup Java
|
|
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5
|
|
with:
|
|
distribution: 'temurin'
|
|
java-version: 21
|
|
- name: Setup Gradle
|
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
|
# - name: Build with Gradle
|
|
# run: ./gradlew build
|
|
- name: Setup android-sdk
|
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
|
with:
|
|
accept-android-sdk-licenses: true
|
|
log-accepted-android-sdk-licenses: true #make accepting the android sdk license verbose
|
|
packages: 'platform-tools platforms;android-36 build-tools;35.0.0'
|
|
|
|
- name: Get npm cache directory
|
|
id: npm-cache-dir
|
|
run: |
|
|
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
|
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
|
|
id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
|
|
with:
|
|
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
|
key: ${{ runner.os }}-node22-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-node22-
|
|
- name: Install npm Packages
|
|
# if: steps.npm-cache.outputs.cache-hit != 'true'
|
|
run: npm i
|
|
|
|
- name: Decode Keystore
|
|
env:
|
|
RELEASE_KEYSTORE_PASSWORD: ${{ secrets.DROID_KEYSTORE_PASSWORD }}
|
|
RELEASE_KEYSTORE_ALIAS: ${{ secrets.DROID_KEYSTORE_ALIAS }}
|
|
RELEASE_KEY_PASSWORD: ${{ secrets.DROID_KEY_PASSWORD }}
|
|
run: |
|
|
echo "${{ secrets.DROID_KEYSTORE_BASE_64 }}" | base64 --decode > keystore.jks
|
|
|
|
# --- Per-push dev build (non-tag master pushes only) --------------------
|
|
# On plain master pushes the release-signed APK is otherwise artifact-only;
|
|
# here we stamp it with a dev versionCode and (after Build) ship it to the
|
|
# Play `internal` track so a fresh build auto-updates on the phone. All
|
|
# steps are gated to refs/heads/master (non-tag), so the tag/release path
|
|
# is untouched. Nothing is committed — the versionCode is only sed'd into
|
|
# the working copy of build.gradle at build time.
|
|
- name: Compute dev versionCode
|
|
id: devvc
|
|
# Dev distribution must never redden the release-critical build; on any
|
|
# failure the downstream gates read '' and simply skip.
|
|
continue-on-error: true
|
|
if: github.ref == 'refs/heads/master'
|
|
run: node tools/android-dev-version-code.js
|
|
- name: Apply dev versionCode
|
|
id: applyvc
|
|
# continue-on-error: a dev-versioning hiccup must never break the
|
|
# release-critical build; on failure `applied` stays unset and the Play
|
|
# upload below is skipped (the artifact build still succeeds).
|
|
continue-on-error: true
|
|
if: github.ref == 'refs/heads/master' && steps.devvc.outputs.skip == 'false'
|
|
env:
|
|
CODE: ${{ steps.devvc.outputs.code }}
|
|
run: |
|
|
set -euo pipefail
|
|
sed -i -E "s/versionCode [0-9_]+/versionCode ${CODE}/" android/app/build.gradle
|
|
grep -qE "versionCode ${CODE}( |$)" android/app/build.gradle \
|
|
|| { echo "::error::versionCode override did not apply"; exit 1; }
|
|
grep -nE 'versionCode [0-9_]+' android/app/build.gradle
|
|
echo "applied=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build
|
|
run: npm run dist:android:prod
|
|
env:
|
|
RELEASE_KEYSTORE_PASSWORD: ${{ secrets.DROID_KEYSTORE_PASSWORD }}
|
|
RELEASE_KEYSTORE_ALIAS: ${{ secrets.DROID_KEYSTORE_ALIAS }}
|
|
RELEASE_KEY_PASSWORD: ${{ secrets.DROID_KEY_PASSWORD }}
|
|
|
|
- name: DEBUG
|
|
run: |
|
|
ls -la android/app/build/outputs
|
|
ls -la android/app/build/outputs/apk
|
|
ls -la android/app/build/outputs/apk/play
|
|
ls -la android/app/build/outputs/apk/play/release
|
|
|
|
# APK is now signed automatically by Gradle using signingConfig
|
|
|
|
- name: 'Upload APK files'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: sup-android-release
|
|
path: android/app/build/outputs/apk/**/*.apk
|
|
|
|
# Ship the dev-versioned APK to the Play `internal` track (non-tag master
|
|
# pushes only, and only when the versionCode was actually applied). This is
|
|
# the same track the tag build stages real releases on below; dev builds
|
|
# (code base+1..999) and releases (base+9000) stay distinct by versionCode,
|
|
# so they interleave on the track without colliding. Chosen over a closed
|
|
# track because internal testing skips review, so builds land in minutes.
|
|
- name: Upload dev build to Play (internal track)
|
|
# continue-on-error: re-runs (same commit -> same versionCode, which Play
|
|
# rejects as already used), the post-release window (a just-tagged release
|
|
# has a higher code on this track until the tag propagates to the dev
|
|
# step — closed by `git push --follow-tags`), and transient Play API
|
|
# errors must not fail the release-critical workflow.
|
|
continue-on-error: true
|
|
if: github.ref == 'refs/heads/master' && steps.applyvc.outputs.applied == 'true'
|
|
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
|
with:
|
|
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
|
packageName: com.superproductivity.superproductivity
|
|
releaseFiles: android/app/build/outputs/apk/play/release/*.apk
|
|
releaseName: 'dev ${{ steps.devvc.outputs.code }} (${{ steps.devvc.outputs.sha }})'
|
|
tracks: internal
|
|
status: completed
|
|
|
|
- name: Wait for main release to be created
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
run: |
|
|
echo "Waiting for main release to be created..."
|
|
for i in {1..40}; do
|
|
if gh release view ${{ github.ref_name }} --repo ${{ github.repository }} >/dev/null 2>&1; then
|
|
echo "Release found!"
|
|
break
|
|
fi
|
|
echo "Waiting for release... (attempt $i/40)"
|
|
sleep 30
|
|
done
|
|
|
|
# Fail if release still doesn't exist
|
|
if ! gh release view ${{ github.ref_name }} --repo ${{ github.repository }} >/dev/null 2>&1; then
|
|
echo "ERROR: Release ${{ github.ref_name }} was not created after 20 minutes"
|
|
exit 1
|
|
fi
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Upload APK to existing GitHub Release
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
run: |
|
|
for apk in android/app/build/outputs/apk/play/release/*.apk; do
|
|
if [ -f "$apk" ]; then
|
|
echo "Uploading $apk to release ${{ github.ref_name }}"
|
|
gh release upload ${{ github.ref_name }} "$apk" --repo ${{ github.repository }}
|
|
fi
|
|
done
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Prepare Google Play release notes
|
|
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
|
|
run: npm run release-notes:prepare-play-store
|
|
|
|
- name: Upload to Google Play Console
|
|
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
|
|
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
|
with:
|
|
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
|
packageName: com.superproductivity.superproductivity
|
|
releaseFiles: android/app/build/outputs/apk/play/release/*.apk
|
|
tracks: internal
|
|
status: completed
|
|
inAppUpdatePriority: 2
|
|
whatsNewDirectory: build/generated-release-notes/play-store
|