Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Release APKs
name: Release Android artifacts

on:
push:
Expand All @@ -14,7 +14,7 @@ concurrency:

jobs:
release:
name: Build and publish signed APKs
name: Build and publish signed Android artifacts
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
Expand Down Expand Up @@ -88,11 +88,18 @@ jobs:
MEGAPROXY_GIT_COMMIT: ${{ github.sha }}
run: ./scripts/build-release-apks.sh

- name: Build signed App Bundle
env:
MEGAPROXY_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
MEGAPROXY_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
MEGAPROXY_GIT_COMMIT: ${{ github.sha }}
run: ./scripts/build-release-bundle.sh

- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
assets=(dist/release/*.apk dist/release/SHA256SUMS)
assets=(dist/release/*.apk dist/release/*.aab dist/release/SHA256SUMS)
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" "${assets[@]}" \
--repo "$GITHUB_REPOSITORY" \
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,18 @@ universal APK used for reproducible F-Droid verification:

```shell
./scripts/build-release-apks.sh
./scripts/build-release-bundle.sh
```

The script reads its default signing key from `$HOME/AndroidApkKey` and its password from
The scripts read the default signing key from `$HOME/AndroidApkKey` and its password from
`$HOME/.my-tokens/android-key-password`. Override these with `MEGAPROXY_KEYSTORE_PATH`,
`MEGAPROXY_KEY_ALIAS`, `MEGAPROXY_KEY_PASSWORD_FILE`, and `MEGAPROXY_KEY_PASSWORD`. Outputs and
`SHA256SUMS` are written to `dist/release`.
`MEGAPROXY_KEY_ALIAS`, `MEGAPROXY_KEY_PASSWORD_FILE`, and `MEGAPROXY_KEY_PASSWORD`. The signed
ABI-specific and universal APKs, the signed universal App Bundle, and `SHA256SUMS` are
written to `dist/release`. The App Bundle contains every supported ABI; app stores generate and
serve optimized device-specific APK splits from it.

Pushing a version tag runs the GitHub release workflow, builds and verifies every APK, and attaches
the artifacts to a GitHub Release. The tag must match `versionName` exactly:
Pushing a version tag runs the GitHub release workflow, builds and verifies every APK and the App
Bundle, and attaches the artifacts to a GitHub Release. The tag must match `versionName` exactly:

```shell
git tag v0.0.4
Expand Down
147 changes: 147 additions & 0 deletions scripts/build-release-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env bash
set -euo pipefail

project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

if [[ -f "$HOME/.zshrc.extra" ]]; then
source "$HOME/.zshrc.extra"
fi

: "${JAVA_HOME:=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home}"
: "${ANDROID_HOME:=$HOME/Library/Android/sdk}"
: "${ANDROID_NDK_HOME:=$ANDROID_HOME/ndk/29.0.14206865}"
: "${MEGAPROXY_KEYSTORE_PATH:=$HOME/AndroidApkKey}"
: "${MEGAPROXY_KEY_PASSWORD_FILE:=$HOME/.my-tokens/android-key-password}"
: "${MEGAPROXY_KEY_ALIAS:=key0}"
: "${MEGAPROXY_RELEASE_DIR:=$project_dir/dist/release}"
: "${MEGAPROXY_EXPECTED_CERT_SHA256:=8a014a2a558a75b5f900ee0c33cd50f24b7432734912406699fc08866747f822}"

export JAVA_HOME ANDROID_HOME ANDROID_NDK_HOME
export PATH="$JAVA_HOME/bin:$HOME/go/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"

for command in go gomobile java jarsigner keytool unzip; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Required command is unavailable: $command" >&2
exit 1
fi
done

if [[ ! -x "$project_dir/gradlew" ]]; then
echo "Gradle wrapper is missing or not executable: $project_dir/gradlew" >&2
exit 1
fi
if [[ ! -f "$MEGAPROXY_KEYSTORE_PATH" ]]; then
echo "Release keystore is missing: $MEGAPROXY_KEYSTORE_PATH" >&2
exit 1
fi
if [[ ! -f "$MEGAPROXY_KEY_PASSWORD_FILE" ]]; then
echo "Keystore password file is missing: $MEGAPROXY_KEY_PASSWORD_FILE" >&2
exit 1
fi
if [[ ! -d "$ANDROID_NDK_HOME" ]]; then
echo "Android NDK is missing: $ANDROID_NDK_HOME" >&2
exit 1
fi

keystore_password="$(<"$MEGAPROXY_KEY_PASSWORD_FILE")"
if [[ -z "$keystore_password" ]]; then
echo "Keystore password file is empty" >&2
exit 1
fi
: "${MEGAPROXY_KEY_PASSWORD:=$keystore_password}"

keytool -list \
-keystore "$MEGAPROXY_KEYSTORE_PATH" \
-storepass:file "$MEGAPROXY_KEY_PASSWORD_FILE" \
-alias "$MEGAPROXY_KEY_ALIAS" >/dev/null

mkdir -p "$MEGAPROXY_RELEASE_DIR" "$project_dir/app/libs"
find "$MEGAPROXY_RELEASE_DIR" -maxdepth 1 -type f -name 'mega-proxy.aab' -delete

temporary_dir="$(mktemp -d "${TMPDIR:-/tmp}/megaproxy-bundle.XXXXXX")"
original_aar="$project_dir/app/libs/megaproxy.aar"
had_original_aar=false
if [[ -f "$original_aar" ]]; then
cp "$original_aar" "$temporary_dir/megaproxy.aar"
had_original_aar=true
fi
restore_workspace() {
if [[ "$had_original_aar" == true ]]; then
cp "$temporary_dir/megaproxy.aar" "$original_aar"
else
rm -f "$original_aar"
fi
rm -rf "$temporary_dir"
}
trap restore_workspace EXIT

export MEGAPROXY_KEYSTORE_PATH
export MEGAPROXY_KEYSTORE_PASSWORD="$keystore_password"
export MEGAPROXY_KEY_ALIAS
export MEGAPROXY_KEY_PASSWORD

echo "Building universal native AAR for the App Bundle"
(
cd "$project_dir/native"
gomobile bind \
-target=android \
-androidapi 26 \
-trimpath \
-ldflags="-s -w -buildid=" \
-o ../app/libs/megaproxy.aar \
./mobile
)

echo "Building signed App Bundle"
(
cd "$project_dir"
./gradlew clean
./gradlew bundleRelease -PmegaproxyVersionVariant=universal
)

built_bundle="$project_dir/app/build/outputs/bundle/release/app-release.aab"
output_bundle="$MEGAPROXY_RELEASE_DIR/mega-proxy.aab"
if [[ ! -f "$built_bundle" ]]; then
echo "Gradle did not produce the expected App Bundle: $built_bundle" >&2
exit 1
fi
cp "$built_bundle" "$output_bundle"

jarsigner -verify "$output_bundle" >/dev/null
actual_fingerprint="$(
keytool -printcert -jarfile "$output_bundle" \
| awk '/SHA256:/{print $2; exit}' \
| tr -d ':' \
| tr '[:upper:]' '[:lower:]'
)"
if [[ "$actual_fingerprint" != "$MEGAPROXY_EXPECTED_CERT_SHA256" ]]; then
echo "Unexpected signing certificate for $output_bundle: $actual_fingerprint" >&2
exit 1
fi

expected_abis=(arm64-v8a armeabi-v7a x86 x86_64)
bundle_entries="$temporary_dir/bundle-entries.txt"
unzip -Z1 "$output_bundle" > "$bundle_entries"
for abi in "${expected_abis[@]}"; do
for library in libandroidx.graphics.path.so libgojni.so; do
entry="base/lib/$abi/$library"
if ! grep -Fxq "$entry" "$bundle_entries"; then
echo "App Bundle is missing $entry" >&2
exit 1
fi
done
done

(
cd "$MEGAPROXY_RELEASE_DIR"
checksum_files=()
while IFS= read -r file; do
checksum_files+=("$file")
done < <(find . -maxdepth 1 -type f \( -name 'mega-proxy-*.apk' -o -name 'mega-proxy.aab' \) -print | sort)
shasum -a 256 "${checksum_files[@]}" > SHA256SUMS
)

echo
echo "Signed release App Bundle:"
ls -lh "$output_bundle"
echo "Checksums: $MEGAPROXY_RELEASE_DIR/SHA256SUMS"