Parse optional -rc.N and encode versionCode so RCs sort below the matching final release. Co-authored-by: Cursor <cursoragent@cursor.com>
267 lines
10 KiB
YAML
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}"
|