Skip to content
Merged
96 changes: 96 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: android-build

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:

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

env:
NDK_VERSION: "27.2.12479018"

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- name: Set up Android SDK
uses: android-actions/setup-android@v3

- name: Install SDK components
run: |
yes | sdkmanager --licenses >/dev/null || true
sdkmanager --update >/dev/null
sdkmanager --install "platforms;android-37.1" "platforms;android-37.0" "build-tools;37.0.0" "ndk;${{ env.NDK_VERSION }}"

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android

- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: notescribe-core

- name: Install Rust Android tooling
run: |
cargo install cargo-ndk --version 3.5.4 --locked
cargo install uniffi --version 0.32.0 --features cli --locked

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Restore signing key
id: signing
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
if [ -z "$KEYSTORE_BASE64" ]; then
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "KEYSTORE_BASE64 is not set; the release build will be unsigned."
exit 0
fi
printf '%s' "$KEYSTORE_BASE64" | base64 -d > /tmp/release.keystore
printf 'keyAlias=%s\nkeyPassword=%s\nstorePassword=%s\n' \
"$KEY_ALIAS" "$KEY_PASSWORD" "$KEYSTORE_PASSWORD" > /tmp/sign.properties
echo "ready=true" >> "$GITHUB_OUTPUT"

- name: Build release APKs
run: ./gradlew :app:assembleRelease --no-daemon

- name: Verify APK signatures
if: steps.signing.outputs.ready == 'true'
run: |
apksigner="$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)/apksigner"
for apk in app/build/outputs/apk/*/release/*.apk; do
echo "== $apk =="
"$apksigner" verify --print-certs "$apk"
done

- name: Upload APKs
uses: actions/upload-artifact@v4
with:
name: apk
path: app/build/outputs/apk/*/release/*.apk
if-no-files-found: error
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,18 @@

# NoteScribe

An Android notes application with a clear boundary between what the user sees and what does the actual work. The Jetpack Compose layer handles the interface; a Rust core — bridged via JNI — owns the storage, encryption, and business logic underneath it.
An Android notes application with a clear boundary between what the user sees and what does the actual work. The Jetpack Compose layer handles the interface; a Rust core — bridged via JNI — owns the storage, encryption, and business logic underneath it.

## CI builds

Every push to `main` builds a signed release APK on GitHub Actions. The
signing key lives in the repo secrets and never touches the repository.

```sh
scripts/get-apk.sh [DEST_DIR] [arm64|universal|all]
```

Downloads the latest release APK from the workflow artifacts into
`DEST_DIR` (default `~/NoteScribe-builds`) as `NoteScribe-<flavor>-latest.apk`
so re-running the script updates the same file. Pass `NOTESCRIBE_INSTALL=1`
to install it over the current build.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ val abiToTarget = mapOf(

val ndkVersion = "27.2.12479018"
val localPropsFile = rootProject.file("local.properties")
val sdkDir = if (localPropsFile.exists()) {
val sdkDir = (if (localPropsFile.exists()) {
localPropsFile.readLines()
.firstOrNull { it.startsWith("sdk.dir=") }
?.removePrefix("sdk.dir=")
?.trim()
} ?: System.getenv("ANDROID_HOME") ?: "/home/amr/Android/Sdk"
} else null) ?: System.getenv("ANDROID_HOME") ?: "/home/amr/Android/Sdk"
val ndkPath = "$sdkDir/ndk/$ndkVersion"

tasks.register<Exec>("cargoBuildHost") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ class HomeViewModel : ViewModel() {
}

private fun onAddNoteClick() {
_state.update { it.copy(isNavigatingToCreate = true) }
_state.value = _state.value.copy(isNavigatingToCreate = true)
}

private fun onNavigatedToCreate() {
_state.update { it.copy(isNavigatingToCreate = false) }
_state.value = _state.value.copy(isNavigatingToCreate = false)
}
}
49 changes: 49 additions & 0 deletions scripts/get-apk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail

REPO="${NOTESCRIBE_REPO:-ulite-Amr/NoteScribe}"
WORKFLOW="android-build.yml"
DEST_DIR="${1:-${APK_DEST_DIR:-$HOME/NoteScribe-builds}}"
FLAVOR="${2:-${APK_FLAVOR:-arm64}}"

command -v gh >/dev/null 2>&1 || { echo "error: gh CLI not found" >&2; exit 1; }
gh auth status >/dev/null 2>&1 || { echo "error: gh is not authenticated" >&2; exit 1; }

run="$(gh run list --repo "$REPO" --workflow "$WORKFLOW" --branch main --status success --json databaseId --jq '.[0].databaseId')"
[ -n "$run" ] || { echo "error: no successful run of $WORKFLOW on main" >&2; exit 1; }

mkdir -p "$DEST_DIR"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

echo "Downloading apk artifact from run #$run ..."
gh run download "$run" --repo "$REPO" -n apk --dir "$tmp"

installed=()
for apk in "$tmp"/*/release/*.apk; do
name="$(basename "$apk")"
case "$FLAVOR" in
all) ;;
*)
case "$name" in
"app-$FLAVOR-release"*) ;;
*) continue ;;
esac
;;
esac
latest="$DEST_DIR/NoteScribe-$FLAVOR-latest.apk"
cp "$apk" "$latest"
cp "$apk" "$DEST_DIR/NoteScribe-$FLAVOR-run$run-$(date +%Y%m%d-%H%M%S).apk"
echo "Saved: $latest"
installed+=("$latest")
done

[ "${#installed[@]}" -gt 0 ] || { echo "error: no app-$FLAVOR-release APK on run #$run" >&2; exit 1; }

if [ "${NOTESCRIBE_INSTALL:-0}" = "1" ]; then
if command -v adb >/dev/null 2>&1; then
adb install -r "${installed[0]}"
else
pm install -r "${installed[0]}"
fi
fi
Loading