soul-browser/.github/workflows/build-apk.yml
KaKi87 733e8ee6f8 Allow release-candidate versions in the CI VERSION file.
Parse optional -rc.N and encode versionCode so RCs sort below the matching final release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 04:17:49 +02:00

267 lines
10 KiB
YAML

name: Build APK
on:
push:
branches:
- '**'
# Report-only commits from the Exodus job must not rebuild the APK.
paths-ignore:
- 'exodus_privacy_report_v2_latest.txt'
workflow_dispatch:
jobs:
push-i18n-english:
# feature/push-i18n-english is the rollout branch for this job; remove it after merge to main.
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/push-i18n-english'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Publish English strings to i18n repository
env:
I18N_GITHUB_TOKEN: ${{ secrets.I18N_GITHUB_TOKEN }}
run: |
set -euo pipefail
if [[ -z "${I18N_GITHUB_TOKEN}" ]]; then
echo "Missing repository secret I18N_GITHUB_TOKEN (repo-scoped PAT with write access to KaKi87/soul-browser-i18n)." >&2
exit 1
fi
python3 scripts/push-i18n-english.py
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set app version
run: |
set -euo pipefail
VERSION="$(tr -d '[:space:]' < VERSION)"
# Accept MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-rc.N (N = 1..98).
if [[ ! "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-rc\.([0-9]+))?$ ]]; then
echo "VERSION must be MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-rc.N, got: ${VERSION}" >&2
exit 1
fi
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
RC="${BASH_REMATCH[5]:-}"
# Encode so X.Y.Z-rc.N < X.Y.Z, with room for patch/minor:
# MAJOR*1_000_000 + MINOR*10_000 + PATCH*100 + (RC or 99)
VERSION_CODE_BASE=$((MAJOR * 1000000 + MINOR * 10000 + PATCH * 100))
if [[ -n "$RC" ]]; then
if (( 10#${RC} < 1 || 10#${RC} > 98 )); then
echo "RC number must be between 1 and 98, got: ${RC}" >&2
exit 1
fi
VERSION_CODE_BASE=$((VERSION_CODE_BASE + 10#${RC}))
else
VERSION_CODE_BASE=$((VERSION_CODE_BASE + 99))
fi
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
VERSION_NAME="$VERSION"
VERSION_CODE="$VERSION_CODE_BASE"
echo "SOUL_PACKAGE_ID=net.kaki87.soul2" >> "$GITHUB_ENV"
else
TESTING_VERSION="${{ github.run_number }}${{ github.run_attempt }}"
VERSION_NAME="${VERSION}-testing.${TESTING_VERSION}"
VERSION_CODE=$((VERSION_CODE_BASE + TESTING_VERSION))
echo "SOUL_PACKAGE_ID=net.kaki87.soul2.testing" >> "$GITHUB_ENV"
fi
echo "versionName=${VERSION_NAME}"
echo "versionCode=${VERSION_CODE}"
sed -i "s/^ versionCode:.*/ versionCode: ${VERSION_CODE}/" app/apktool.yml
sed -i "s/^ versionName:.*/ versionName: ${VERSION_NAME}/" app/apktool.yml
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Install Android build-tools
run: sdkmanager "build-tools;35.0.0"
- name: Prepare release signing keystore
env:
SOUL_KEYSTORE_BASE64: ${{ secrets.SOUL_KEYSTORE_BASE64 }}
run: |
set -euo pipefail
if [[ -z "${SOUL_KEYSTORE_BASE64}" ]]; then
echo "Missing repository secret SOUL_KEYSTORE_BASE64 (base64-encoded PKCS12/JKS)." >&2
exit 1
fi
mkdir -p keystore
# Avoid echoing secret material; write decoded bytes only.
printf '%s' "${SOUL_KEYSTORE_BASE64}" | base64 -d > keystore/release.keystore
chmod 600 keystore/release.keystore
echo "SOUL_KEYSTORE=${GITHUB_WORKSPACE}/keystore/release.keystore" >> "${GITHUB_ENV}"
- name: Build APK
env:
SOUL_KEY_ALIAS: ${{ secrets.SOUL_KEY_ALIAS }}
SOUL_KEYSTORE_PASSWORD: ${{ secrets.SOUL_KEYSTORE_PASSWORD }}
SOUL_KEY_PASSWORD: ${{ secrets.SOUL_KEY_PASSWORD }}
run: |
set -euo pipefail
if [[ -z "${SOUL_KEYSTORE:-}" || -z "${SOUL_KEYSTORE_PASSWORD:-}" || -z "${SOUL_KEY_PASSWORD:-}" ]]; then
echo "Missing signing secrets (SOUL_KEYSTORE / SOUL_KEYSTORE_PASSWORD / SOUL_KEY_PASSWORD)." >&2
exit 1
fi
# Default alias matches local/debug builds when the secret is unset.
export SOUL_KEY_ALIAS="${SOUL_KEY_ALIAS:-soulbrowser}"
chmod +x scripts/build.sh
./scripts/build.sh
- name: Remove signing keystore
if: always()
run: rm -f keystore/release.keystore
- name: Resolve signed APK
id: apk
run: |
shopt -s nullglob
candidates=()
for f in dist/soul-browser-*.apk; do
case "$f" in
*-unsigned.apk|*-aligned.apk) ;;
*) candidates+=("$f") ;;
esac
done
if [[ ${#candidates[@]} -ne 1 ]]; then
echo "Expected exactly one signed APK, found: ${candidates[*]:-none}" >&2
ls -la dist/ || true
exit 1
fi
echo "path=${candidates[0]}" >> "$GITHUB_OUTPUT"
- name: Upload APK artifact
id: upload_artifact
continue-on-error: true
uses: actions/upload-artifact@v7
with:
path: ${{ steps.apk.outputs.path }}
archive: false
if-no-files-found: error
- name: Upload APK to R2 (artifact quota fallback)
if: steps.upload_artifact.outcome == 'failure'
env:
# Access Key ID is a variable (not a secret): it is embedded in
# presigned URLs, and GitHub would mask a secret value to "***".
AWS_ACCESS_KEY_ID: ${{ vars.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: https://673b6d22cf5cd200862a9c97dbe22ec6.r2.cloudflarestorage.com
R2_BUCKET: github-actions-artifacts
APK_PATH: ${{ steps.apk.outputs.path }}
run: |
set -euo pipefail
if [[ -z "${AWS_ACCESS_KEY_ID:-}" || -z "${AWS_SECRET_ACCESS_KEY:-}" ]]; then
echo "R2_ACCESS_KEY_ID (variable) / R2_SECRET_ACCESS_KEY (secret) are not configured." >&2
exit 1
fi
filename="$(basename "$APK_PATH")"
key="soul-browser/${{ github.run_id }}/${{ github.run_attempt }}/${filename}"
object_url="${R2_ENDPOINT}/${R2_BUCKET}/${key}"
echo "GitHub artifact upload failed; uploading to Cloudflare R2 as fallback..."
aws s3 cp "$APK_PATH" "s3://${R2_BUCKET}/${key}" --endpoint-url "$R2_ENDPOINT"
# Bucket is private; presign so the step summary link is downloadable.
download_url="$(aws s3 presign "s3://${R2_BUCKET}/${key}" --endpoint-url "$R2_ENDPOINT" --expires-in 604800)"
{
echo "## APK (R2 fallback)"
echo
echo "GitHub Actions artifact upload failed (often: Artifact storage quota has been hit)."
echo
echo "- Object: \`${object_url}\`"
echo "- Download (7-day signed URL): [${filename}](${download_url})"
} >> "$GITHUB_STEP_SUMMARY"
echo "Uploaded to ${object_url}"
# Also print to logs so agents can fetch without opening the Actions UI.
echo "R2_DOWNLOAD_URL=${download_url}"
- name: Analyze APK with Exodus Privacy
# cursor/b2c729ca is the rollout branch for this job; remove it after merge to main.
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/cursor/b2c729ca'
env:
APK_PATH: ${{ steps.apk.outputs.path }}
run: |
set -euo pipefail
apk_name="$(basename "$APK_PATH")"
docker run --rm --pull always \
-v "${GITHUB_WORKSPACE}/dist:/app:ro" \
exodusprivacy/exodus-standalone \
"/app/${apk_name}" \
> exodus_privacy_report_v2_latest.txt
if [[ ! -s exodus_privacy_report_v2_latest.txt ]]; then
echo "Exodus Privacy report is empty." >&2
exit 1
fi
python3 - <<'PY'
from pathlib import Path
path = Path("exodus_privacy_report_v2_latest.txt")
raw = path.read_text()
lines = raw.splitlines()
out = []
i = 0
while i < len(lines):
line = lines[i]
if line.startswith("- App permissions:"):
out.append(line)
i += 1
perms = []
while i < len(lines) and lines[i].startswith(" - "):
perms.append(lines[i])
i += 1
out.extend(sorted(perms))
continue
out.append(line)
i += 1
ending = "\n" if raw.endswith("\n") else ""
path.write_text("\n".join(out) + ending)
PY
cat exodus_privacy_report_v2_latest.txt
- name: Commit Exodus Privacy report
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/cursor/b2c729ca'
run: |
set -euo pipefail
report_file="exodus_privacy_report_v2_latest.txt"
cp "$report_file" /tmp/exodus_privacy_report_v2_latest.txt
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin "${GITHUB_REF_NAME}"
git reset --hard "origin/${GITHUB_REF_NAME}"
cp /tmp/exodus_privacy_report_v2_latest.txt "$report_file"
git add "$report_file"
git commit -m "[skip ci] Update Exodus Privacy report."
git push origin "HEAD:${GITHUB_REF_NAME}"