Skip to content

✨ feat(android): wire sideload import to library-mirror push #180

✨ feat(android): wire sideload import to library-mirror push

✨ feat(android): wire sideload import to library-mirror push #180

Workflow file for this run

name: android-ci
on:
push:
paths:
- "app/**"
- "auth/**"
- "core/**"
- "data/**"
- "reader/**"
- "build.gradle.kts"
- "settings.gradle.kts"
- "gradle.properties"
- "gradle/**"
- "gradlew"
- ".github/workflows/android-ci.yaml"
pull_request:
paths:
- "app/**"
- "auth/**"
- "core/**"
- "data/**"
- "reader/**"
- "build.gradle.kts"
- "settings.gradle.kts"
- "gradle.properties"
- "gradle/**"
- "gradlew"
- ".github/workflows/android-ci.yaml"
# Cancel obsolete runs on PR / branch pushes. Never cancel runs on main —
# we want a Release for every commit that lands.
concurrency:
group: android-ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # to push the version-bump commit and tag
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4
with:
packages: "platform-tools platforms;android-34 build-tools;34.0.0"
- uses: gradle/actions/setup-gradle@4c125117fe7c5aed11272ec4213f602f012f89f2 # v5
- id: version
name: Compute version
env:
# Use the GitHub-provided token to push the bump commit and tag.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "$GITHUB_REF" = "refs/heads/main" ]; then
# On main: compute the next CalVer from date + run number,
# write it into gradle.properties, commit + tag, then push
# both before Gradle runs. The build then reads the bumped
# values directly from gradle.properties.
BUILD_DATE=$(date -u +%Y-%m-%d)
NEW_NAME="${BUILD_DATE//-/.}.${GITHUB_RUN_NUMBER}"
YY=$(date -u +%y)
MM=$(date -u +%m)
DD=$(date -u +%d)
NEW_CODE=$(( (10#$YY * 10000 + 10#$MM * 100 + 10#$DD) * 100 + GITHUB_RUN_NUMBER % 100 ))
sed -i "s/^VERSION_NAME=.*/VERSION_NAME=$NEW_NAME/" gradle.properties
sed -i "s/^VERSION_CODE=.*/VERSION_CODE=$NEW_CODE/" gradle.properties
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
# If another main push lands between our checkout and our push
# (or between two consecutive bump runs), HEAD is non-fast-forward.
# The naive `git rebase` strategy conflicts when both bump commits
# mutate the same `VERSION_NAME=` line — that's unrecoverable
# without manual resolution. Instead, on rejection we reset hard
# to the latest main, re-apply the bump on top, and retry. The
# bump value is derived from GITHUB_RUN_NUMBER which is stable
# across retries, so we always end up with a unique tag.
apply_bump() {
sed -i "s/^VERSION_NAME=.*/VERSION_NAME=$NEW_NAME/" gradle.properties
sed -i "s/^VERSION_CODE=.*/VERSION_CODE=$NEW_CODE/" gradle.properties
git add gradle.properties
git commit -m ":bookmark: chore: release v$NEW_NAME"
git tag -f "v$NEW_NAME"
}
apply_bump
attempts=0
until git push origin "HEAD:$GITHUB_REF" "v$NEW_NAME"; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 5 ]; then
echo "release bump push failed after $attempts attempts" >&2
exit 1
fi
echo "release bump push rejected; resetting + re-applying bump ($attempts)…"
git fetch origin "$GITHUB_REF"
git tag -d "v$NEW_NAME" 2>/dev/null || true
git reset --hard "origin/${GITHUB_REF#refs/heads/}"
apply_bump
done
VERSION="$NEW_NAME"
else
# On PRs / feature branches: just read the static value from
# gradle.properties — no bump, no tag, no commit.
VERSION=$(grep '^VERSION_NAME=' gradle.properties | cut -d= -f2)
fi
echo "VERSION_NAME=$VERSION" >> "$GITHUB_ENV"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building version $VERSION"
- name: Install ripgrep (rename guard)
run: |
if ! command -v rg >/dev/null; then
sudo apt-get update && sudo apt-get install -y ripgrep
fi
- name: Rename guard self-test
run: |
set -e
# Proves the rename-guard regex catches OPDS_SYNC_<X> patterns.
tmp=$(mktemp /tmp/rename-guard-self-test-XXXX.txt)
printf 'OPDS_SYNC_AI_ENABLED: true\n' > "$tmp"
if ! rg -P '\bopds[-_]sync\b|(?<![A-Z0-9_])OPDS_SYNC_[A-Z0-9_]*' "$tmp" >/dev/null; then
echo "::error::Rename guard regex failed self-test (did NOT match OPDS_SYNC_AI_ENABLED)."
rm -f "$tmp"
exit 1
fi
rm -f "$tmp"
- name: Rename guard
run: |
set -o pipefail
# Mirror of the server-ci rename guard. Lives here too because
# server-ci is path-filtered to server/** and would not run for
# Android-side regressions that smuggle in legacy names.
if rg -n -P '\bopds[-_]sync\b|(?<![A-Z0-9_])OPDS_SYNC_[A-Z0-9_]*' \
-g '!.claude/**' \
-g '!CHANGELOG.md' \
-g '!docs/release.md' \
-g '!docs/development.md' \
-g '!server/migrations/versions/*.py' \
-g '!README.md' \
-g '!server/quire_server/config.py' \
-g '!server/docker-compose.yml' \
-g '!server/docker-compose.full.yml' \
-g '!server/UPGRADING.md' \
-g '!server/alembic.ini' \
-g '!server/migrations/env.py' \
-g '!server/.env.example' \
-g '!server/tests/conftest.py' \
-g '!server/tests/unit/test_no_stale_opds_sync_imports.py' \
. ; then
echo "::error::Rename guard found un-allowlisted OPDS_SYNC / opds-sync / opds_sync references."
exit 1
fi
- name: Assemble debug APK
run: ./gradlew assembleDebug --stacktrace
- name: Unit tests
run: ./gradlew test --stacktrace
- name: Lint
run: ./gradlew lint --stacktrace
- name: Upload debug APK
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: app-debug-${{ env.VERSION_NAME }}
path: app/build/outputs/apk/debug/*.apk
if-no-files-found: error
# On main only: build the signed release APK and publish a GitHub
# Release. The release action creates the tag itself, so we don't push
# tags from the workflow (workflow-pushed tags via GITHUB_TOKEN don't
# trigger downstream runs anyway, which is why this used to be split).
release:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
ref: v${{ needs.build.outputs.version }}
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: temurin
java-version: "21"
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4
with:
packages: "platform-tools platforms;android-34 build-tools;34.0.0"
- uses: gradle/actions/setup-gradle@4c125117fe7c5aed11272ec4213f602f012f89f2 # v5
- name: Decode keystore
if: env.KEYSTORE_B64 != ''
run: echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/release.keystore"
env:
KEYSTORE_B64: ${{ secrets.QUIRE_RELEASE_KEYSTORE_B64 }}
- name: Assemble release APK
env:
QUIRE_RELEASE_KEYSTORE: ${{ secrets.QUIRE_RELEASE_KEYSTORE_B64 != '' && format('{0}/release.keystore', runner.temp) || '' }}
QUIRE_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.QUIRE_RELEASE_KEYSTORE_PASSWORD }}
QUIRE_RELEASE_KEY_ALIAS: ${{ secrets.QUIRE_RELEASE_KEY_ALIAS }}
QUIRE_RELEASE_KEY_PASSWORD: ${{ secrets.QUIRE_RELEASE_KEY_PASSWORD }}
run: ./gradlew :app:assembleRelease --stacktrace
- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
tag_name: v${{ needs.build.outputs.version }}
files: app/build/outputs/apk/release/*.apk
generate_release_notes: true