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
26 changes: 25 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
UNIT_TESTS: "true"
Expand Down Expand Up @@ -39,13 +40,30 @@ jobs:
uses: android-actions/setup-android@v3

- name: Install just
run: cargo install just
uses: extractions/setup-just@v2

- name: Cache NDK
uses: actions/cache@v4
with:
path: ${{ env.ANDROID_HOME }}/ndk/28.1.13356709
key: ndk-28.1.13356709-${{ runner.os }}

- name: Setup NDK
run: just install-ndk

- name: Cache AVD
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-29-${{ runner.os }}

- name: Gradle cache
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Build tests
run: just build-test
Expand All @@ -58,4 +76,10 @@ jobs:
with:
api-level: 29
arch: x86_64
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: just test
env:
GPR_USERNAME: ${{ github.actor }}
GPR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94 changes: 44 additions & 50 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ concurrency:

env:
BASE_IMAGE_NAME: gem-android-base
BASE_IMAGE_FULL_NAME: ghcr.io/${{ github.repository_owner }}/gem-android-base
DOCKER_BUILDKIT: 1

jobs:
Expand All @@ -24,6 +25,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image_tag: ${{ steps.meta.outputs.version }}
steps:
Expand All @@ -33,62 +35,58 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata for base image
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.BASE_IMAGE_NAME }}
images: ${{ env.BASE_IMAGE_FULL_NAME }}
# Generate a version tag based on commit SHA. This will be unique.
tags: |
type=sha,format=long,prefix=

- name: Build and load Dockerfile.base
- name: Build and push Dockerfile.base
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.base
push: false
load: true
tags: ${{ env.BASE_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
push: true
tags: ${{ env.BASE_IMAGE_FULL_NAME }}:${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
cache-from: type=gha,scope=base-image
cache-to: type=gha,mode=max,scope=base-image
build-args: |
BUILDKIT_INLINE_CACHE=1

- name: Save base image to tarball
run: docker save ${{ env.BASE_IMAGE_NAME }}:${{ steps.meta.outputs.version }} -o base_image.tar

- name: Upload base image artifact
uses: actions/upload-artifact@v4
with:
name: base-image-artifact
path: base_image.tar
retention-days: 1

build_app_image:
name: Build App Docker Image
needs: build_base_image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Download base image artifact
uses: actions/download-artifact@v4
with:
name: base-image-artifact

- name: Load base image from tarball
run: docker load -i base_image.tar

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
driver: docker
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create local.properties for CI
run: |
Expand All @@ -97,36 +95,32 @@ jobs:
echo "# Automatically generated for CI build" >> local.properties

- name: Build Dockerfile.app
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.app
build-args: |
TAG=${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
BASE_IMAGE_TAG=${{ needs.build_base_image.outputs.image_tag }}
SKIP_SIGN=true
push: false
load: true
tags: gem-android-app:latest
platforms: linux/amd64

- name: Extract AAB from Docker container
env:
DOCKER_BUILDKIT: 1
run: |
docker buildx build \
--build-arg TAG=${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} \
--build-arg BASE_IMAGE=${{ env.BASE_IMAGE_FULL_NAME }} \
--build-arg BASE_IMAGE_TAG=${{ needs.build_base_image.outputs.image_tag }} \
--build-arg SKIP_SIGN=true \
--cache-from type=gha,scope=app-image \
--cache-to type=gha,mode=max,scope=app-image \
--load \
--tag gem-android-app:latest \
--file ./Dockerfile.app \
.

- name: Extract Universal APK from Docker container
run: |
# Create container from the built image
CONTAINER_ID=$(docker create gem-android-app:latest)

# Copy AAB files from container to host
docker cp $CONTAINER_ID:/root/gem-android/app/build/outputs/bundle/googleRelease/ ./aab-output/

# Clean up container
mkdir -p apk-output
docker cp $CONTAINER_ID:/root/gem-android/app/build/outputs/apk/universal/release/. ./apk-output/
docker rm $CONTAINER_ID

# List the extracted files
find ./aab-output -name "*.aab"
find ./apk-output -name "*.apk"

- name: Upload AAB artifacts
- name: Upload APK artifacts
uses: actions/upload-artifact@v4
with:
name: gem-android-aab
path: ./aab-output/
name: gem-android-apk
path: ./apk-output/
retention-days: 7
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.DS_Store
.vscode/
*~
artifacts/

#ANDROID
# Gradle files
Expand Down
15 changes: 9 additions & 6 deletions Dockerfile.app
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# syntax=docker/dockerfile:1.4
# This Dockerfile is used to build the Android app (no signing).
ARG BASE_IMAGE=gem-android-base
ARG BASE_IMAGE_TAG=latest
FROM gem-android-base:${BASE_IMAGE_TAG}
FROM ${BASE_IMAGE}:${BASE_IMAGE_TAG}

# Arguments for current tag and skip sign
ARG TAG=main
ARG SKIP_SIGN
ARG BUNDLE_TASK=":app:assembleUniversalRelease"

# Check if branch/tag exists and clone accordingly
RUN REPO_URL="https://github.com/gemwalletcom/gem-android.git" && \
if git ls-remote --exit-code --heads "$REPO_URL" "$TAG" >/dev/null 2>&1; then \
echo "Branch $TAG exists, cloning it..." && \
Expand All @@ -21,10 +22,12 @@ RUN REPO_URL="https://github.com/gemwalletcom/gem-android.git" && \

WORKDIR $HOME/gem-android

# Copy local.properties from the build context. For local builds, this is your local file. CI builds, this file is created by a previous workflow step.
COPY --chown=root:root local.properties ./local.properties

# Build the application
RUN export SKIP_SIGN=${SKIP_SIGN} && just unsigned-release
RUN --mount=type=cache,target=/root/.gradle \
--mount=type=cache,target=/root/.m2 \
--mount=type=cache,target=/root/.cargo \
export SKIP_SIGN=${SKIP_SIGN} && \
./gradlew ${BUNDLE_TASK} --no-daemon --build-cache

CMD ["bash"]
52 changes: 26 additions & 26 deletions Dockerfile.base
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# syntax=docker/dockerfile:1.4
# This Dockerfile is used to setup the environment (JDK, SDK, NDK, just) for Android app building.

FROM debian:bookworm

# Set environment variables for non-interactive installation
ENV DEBIAN_FRONTEND=noninteractive
ENV HOME=/root
ENV ANDROID_HOME=/opt/android-sdk
ENV ANDROID_SDK_ROOT=/opt/android-sdk
ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}

# Enable multi-arch
RUN dpkg --add-architecture amd64

# Install necessary tools
RUN apt-get update && apt-get install -y \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y \
curl \
unzip \
zip \
Expand All @@ -19,30 +23,26 @@ RUN apt-get update && apt-get install -y \
pkg-config \
openjdk-17-jdk-headless \
libc6:amd64 \
zlib1g:amd64 \
&& rm -rf /var/lib/apt/lists/*
zlib1g:amd64

# Install just using the official script
RUN curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin

ENV HOME /root

# Set environment variables for Android SDK
ENV ANDROID_HOME /opt/android-sdk
ENV ANDROID_SDK_ROOT /opt/android-sdk
ENV PATH ${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/cmdline-tools/bin:${ANDROID_HOME}/platform-tools:${PATH}

# Download and install Android SDK command-line tools
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \
curl -o sdk-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip && \
unzip sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools && \
# The zip extracts to a 'cmdline-tools' directory, so we move its contents up.
mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools/* ${ANDROID_HOME}/cmdline-tools/ && \
rm -rf ${ANDROID_HOME}/cmdline-tools/cmdline-tools && \
rm sdk-tools.zip

# Accept licenses and install SDK components
RUN yes | ${ANDROID_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --licenses && \
${ANDROID_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "platform-tools" "platforms;android-35" "build-tools;35.0.0" "ndk;28.1.13356709"
RUN --mount=type=cache,target=/tmp/android-dl \
mkdir -p ${ANDROID_HOME}/cmdline-tools/latest && \
if [ ! -f /tmp/android-dl/commandlinetools-linux-11076708_latest.zip ]; then \
curl -o /tmp/android-dl/commandlinetools-linux-11076708_latest.zip \
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip; \
fi && \
unzip -q /tmp/android-dl/commandlinetools-linux-11076708_latest.zip -d /tmp/cmdline-tools && \
mv /tmp/cmdline-tools/cmdline-tools/* ${ANDROID_HOME}/cmdline-tools/latest/ && \
rm -rf /tmp/cmdline-tools

RUN --mount=type=cache,target=/root/.android \
yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --licenses && \
${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager \
"platform-tools" \
"platforms;android-35" \
"build-tools;35.0.0" \
"ndk;28.1.13356709"

CMD ["bash"]
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ We run [MobSF mobsfscan](https://github.com/MobSF/mobsfscan) to catch insecure p

Only suppress detections when you fully understand the risk—ideally fix the code; otherwise, add a targeted `// mobsf-ignore: rule_id` comment with context.

## ♻️ Reproducible Release Verification

Use the helper script to rebuild a tagged release in Docker and compare it against an APK you downloaded from a trusted endpoint (e.g., the GitHub release assets). By default it runs `:app:assembleUniversalRelease` and copies the resulting APK from `app/build/outputs/apk/universal/release`; override `VERIFY_GRADLE_TASK` / `VERIFY_APK_SUBDIR` if you need another flavor.

```bash
curl -L --fail -o official.apk http://apk.gemwallet.com/gem_wallet_universal_v1.3.48.apk
./scripts/verify_apk.sh v1.3.48 official.apk
```

The script will:

- (re)build the `gem-android-base` image if necessary;
- build `Dockerfile.app` for the requested git tag/branch using the selected Gradle task;
- copy the produced APK directly out of `app/build/outputs/apk/...`; and
- store the rebuilt + official APKs plus their hashes in `artifacts/reproducible/<tag>/`.

If the bytes match, the script exits with success; otherwise it returns status code `2`. Set `VERIFY_ALLOW_MISMATCH=true` when you only need the artifacts/hashes (our CI job uses this to avoid red builds while still surfacing the comparison result). When [`diffoscope`](https://diffoscope.org/) is available, the script also unzips both APKs (stripping signing metadata) and writes an HTML diff report to `artifacts/reproducible/<tag>/diffoscope.html`; set `VERIFY_SKIP_DIFFOSCOPE=true` to skip generating this report.

## 👨‍👧‍👦 Contributors

We love contributors! Feel free to contribute to this project but please read the [Contributing Guidelines](CONTRIBUTING.md) first!
Expand Down
15 changes: 14 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,21 @@ allprojects {
}
}
}

dependencyLocking {
lockAllConfigurations()
}
}

subprojects {
dependencyLocking {
lockAllConfigurations()
}
configurations.configureEach {
resolutionStrategy.activateDependencyLocking()
}
}

tasks.register("clean", Delete::class) {
delete(layout.buildDirectory)
}
}
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ material3 = "1.5.0-alpha07"
navigationCompose = "2.9.5"
uiTestJunit4 = "1.9.4"
testRunner = "1.7.0"
ksp = "2.1.21-2.0.1"
ksp = "2.2.21-2.0.4"
android-lib = "8.13.0"
kotlin = "2.2.21"
google-services = "4.4.4"
Expand Down Expand Up @@ -172,4 +172,4 @@ android-library = { id = "com.android.library", version.ref = "android-lib" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
google-services = { id = "com.google.gms.google-services", version.ref = "google-services" }
room = { id = "androidx.room", version.ref = "room" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Loading
Loading