Skip to content
Open
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
106 changes: 106 additions & 0 deletions .github/workflows/android-demos-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Android Demos Build
on:
push:
branches: [ main ]
paths:
- 'Android/SloganApp/**'
- 'Android/LeapAudioDemo/**'
- 'Android/ShareAI/**'
- 'Android/RecipeGenerator/**'
- 'Android/VLMExample/**'
- 'Android/LeapVoiceAssistantDemo/**'
- 'Android/LeapKoogAgent/**'
- '.github/workflows/android-demos-test.yml'
pull_request:
branches: [ main ]
paths:
- 'Android/SloganApp/**'
- 'Android/LeapAudioDemo/**'
- 'Android/ShareAI/**'
- 'Android/RecipeGenerator/**'
- 'Android/VLMExample/**'
- 'Android/LeapVoiceAssistantDemo/**'
- 'Android/LeapKoogAgent/**'
- '.github/workflows/android-demos-test.yml'
workflow_dispatch:

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

# Least-privilege default. The job only pulls Maven Central artifacts and
# assembles a debug APK; no GitHub API write scope required.
permissions:
contents: read

jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# LeapKoogAgent excluded: requires vendored koog-edge-0.0.1.aar not checked in.
# See Android/LeapKoogAgent/README.md.
demo:
- SloganApp
- LeapAudioDemo
- ShareAI
- RecipeGenerator
- VLMExample
- LeapVoiceAssistantDemo
defaults:
run:
working-directory: Android/${{ matrix.demo }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Purge cached leap-sdk artifacts if Sonatype snapshot was republished
# The pin may be a SNAPSHOT in which case the upstream may force-update it under
# the same SNAPSHOT version, with new contents. When that happens,
# `--refresh-dependencies` alone isn't enough: the gradle cache restored by
# setup-java's cache: 'gradle' still contains the prior artifact, and gradle
# short-circuits the re-download because the cached SHA matches its metadata.
#
# Detect the situation by comparing Sonatype's <lastUpdated> for the snapshot
# against a marker we wrote on the previous run. Purge only when they differ —
# so the cache stays warm in the common case (no upstream republish) and
# rebuilds get the new artifact in the rare case (snapshot updated).
run: |
SDK_VERSION=$(grep -E '^\s*leapSdk\s*=' gradle/libs.versions.toml | sed -E 's/.*"([^"]+)".*/\1/')
# Whitelist the parsed version before interpolating into the curl URL.
if ! echo "$SDK_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)*(-[A-Za-z0-9.]+)?(-SNAPSHOT)?$'; then
echo "(unexpected leapSdk version shape '$SDK_VERSION' — skipping purge check)"
exit 0
fi
case "$SDK_VERSION" in
*-SNAPSHOT) ;;
*) echo "leap-sdk pinned to stable $SDK_VERSION; skipping snapshot purge check"; exit 0 ;;
esac
REMOTE_TS=$(curl -fsSL "https://central.sonatype.com/repository/maven-snapshots/ai/liquid/leap/leap-sdk/$SDK_VERSION/maven-metadata.xml" 2>/dev/null \
| grep -oE '<lastUpdated>[0-9]+</lastUpdated>' | grep -oE '[0-9]+' || true)
MARKER=~/.gradle/caches/leap-snapshot-marker
LOCAL_TS=$(cat "$MARKER" 2>/dev/null || true)
if [ -z "$REMOTE_TS" ]; then
echo "(could not fetch snapshot metadata for $SDK_VERSION from Sonatype — skipping purge check)"
elif [ "$REMOTE_TS" = "$LOCAL_TS" ]; then
echo "Sonatype snapshot timestamp unchanged for $SDK_VERSION ($LOCAL_TS); leap-sdk cache is fresh"
else
echo "Sonatype snapshot timestamp changed for $SDK_VERSION (${LOCAL_TS:-<none>} -> $REMOTE_TS); purging leap-sdk cache"
rm -rf ~/.gradle/caches/modules-2/files-2.1/ai.liquid.leap* \
~/.gradle/caches/modules-2/metadata-*/descriptors/ai.liquid.leap*
mkdir -p "$(dirname "$MARKER")"
echo "$REMOTE_TS" > "$MARKER"
fi
- name: ktfmt check
run: ./gradlew :app:ktfmtCheck
- name: Assemble debug APK
run: ./gradlew --refresh-dependencies :app:assembleDebug
50 changes: 48 additions & 2 deletions .github/workflows/android-leap-chat-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
- '.github/workflows/android-leap-chat-test.yml'
workflow_dispatch:

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

# Least-privilege default. The job authenticates with gcloud against Firebase
# Test Lab via a service-account JSON; nothing here needs GITHUB_TOKEN write
# scope. The Actions cache (used by gradle/actions/setup-gradle below) is gated
Expand All @@ -33,10 +37,52 @@ jobs:
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Purge cached leap-sdk artifacts if Sonatype snapshot was republished
# The pin may be a SNAPSHOT in which case the upstream may force-update it under
# the same SNAPSHOT version, with new contents. When that happens,
# `--refresh-dependencies` alone isn't enough: the gradle cache restored by
# setup-java's cache: 'gradle' still contains the prior artifact, and gradle
# short-circuits the re-download because the cached SHA matches its metadata.
#
# Detect the situation by comparing Sonatype's <lastUpdated> for the snapshot
# against a marker we wrote on the previous run. Purge only when they differ —
# so the cache stays warm in the common case (no upstream republish) and
# rebuilds get the new artifact in the rare case (snapshot updated).
run: |
SDK_VERSION=$(grep -E '^\s*leapSdk\s*=' Android/LeapChat/gradle/libs.versions.toml | sed -E 's/.*"([^"]+)".*/\1/')
# Whitelist before interpolating into the curl URL below.
if ! echo "$SDK_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)*(-[A-Za-z0-9.]+)?(-SNAPSHOT)?$'; then
echo "(unexpected leapSdk version shape '$SDK_VERSION' — skipping purge check)"
exit 0
fi
case "$SDK_VERSION" in
*-SNAPSHOT) ;;
*) echo "leap-sdk pinned to stable $SDK_VERSION; skipping snapshot purge check"; exit 0 ;;
esac
# Check the parent `leap-sdk` artifact's <lastUpdated> — when the SDK is
# republished, the parent and every per-target sibling get re-uploaded
# together with the same timestamp, so one URL is representative for any
# workflow. Marker lives under ~/.gradle/caches so it's preserved by
# setup-java's gradle cache between runs.
REMOTE_TS=$(curl -fsSL "https://central.sonatype.com/repository/maven-snapshots/ai/liquid/leap/leap-sdk/$SDK_VERSION/maven-metadata.xml" 2>/dev/null \
| grep -oE '<lastUpdated>[0-9]+</lastUpdated>' | grep -oE '[0-9]+' || true)
MARKER=~/.gradle/caches/leap-snapshot-marker
LOCAL_TS=$(cat "$MARKER" 2>/dev/null || true)
if [ -z "$REMOTE_TS" ]; then
echo "(could not fetch snapshot metadata for $SDK_VERSION from Sonatype — skipping purge check)"
elif [ "$REMOTE_TS" = "$LOCAL_TS" ]; then
echo "Sonatype snapshot timestamp unchanged for $SDK_VERSION ($LOCAL_TS); leap-sdk cache is fresh"
else
echo "Sonatype snapshot timestamp changed for $SDK_VERSION (${LOCAL_TS:-<none>} -> $REMOTE_TS); purging leap-sdk cache"
rm -rf ~/.gradle/caches/modules-2/files-2.1/ai.liquid.leap* \
~/.gradle/caches/modules-2/metadata-*/descriptors/ai.liquid.leap*
mkdir -p "$(dirname "$MARKER")"
echo "$REMOTE_TS" > "$MARKER"
fi
- name: Build LeapChat
run: cd Android/LeapChat && ./gradlew :app:assemble
run: cd Android/LeapChat && ./gradlew --refresh-dependencies :app:assemble
- name: Build E2E test
run: cd Android/LeapChat && ./gradlew :app:assembleAndroidTest
run: cd Android/LeapChat && ./gradlew --refresh-dependencies :app:assembleAndroidTest
- name: Run E2E test on Firebase Test Lab
env:
SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
Expand Down
88 changes: 88 additions & 0 deletions .github/workflows/ios-demos-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: iOS Demos Build
on:
push:
branches: [ main ]
paths:
- 'iOS/**'
- '.github/workflows/ios-demos-test.yml'
pull_request:
branches: [ main ]
paths:
- 'iOS/**'
- '.github/workflows/ios-demos-test.yml'
workflow_dispatch:

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

# Least-privilege default. xcodegen + xcodebuild only need to read the workspace
# and pull SPM dependencies from GitHub; no write scopes required.
permissions:
contents: read

jobs:
build:
runs-on: macos-26
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
demo:
- LeapSloganExample
- LeapChatExample
- LeapAudioDemo
- LeapVLMExample
- LeapVoiceAssistantDemo
- RecipeGenerator
defaults:
run:
working-directory: iOS/${{ matrix.demo }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Select Xcode 26
# GitHub-hosted macos-26 ships Xcode 26 by default; pin explicitly to guard against
# image-default drift. Falls back to whatever xcode-select already points at if
# Xcode_26.app isn't present (defensive — should always exist on macos-26).
run: |
if [ -d /Applications/Xcode_26.app ]; then
sudo xcode-select -s /Applications/Xcode_26.app/Contents/Developer
fi
xcodebuild -version
working-directory: .
- name: Install xcodegen
run: brew install xcodegen
working-directory: .
- name: Skip macro fingerprint validation
# LeapSloganExample and RecipeGenerator depend on LeapSDKMacros. Xcode's macro
# fingerprint prompt would block a non-interactive `xcodebuild` run; opt out
# globally for this CI session.
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES
working-directory: .
- name: Clear SPM caches if LeapSDK pin is a SNAPSHOT tag
# SPM caches a force-pushed snapshot tag's old revision and refuses to resolve
# with "does not match previously recorded value". Wipe both SPM caches and
# DerivedData when the demo pins a -SNAPSHOT revision so resolution starts
# clean. No-op for stable tags.
run: |
if grep -qE '^\s*revision:\s*[A-Za-z0-9.\-]+-SNAPSHOT\s*$' project.yml; then
echo "Detected -SNAPSHOT revision; clearing SPM caches"
rm -rf ~/Library/Caches/org.swift.swiftpm \
~/Library/org.swift.swiftpm \
~/Library/Developer/Xcode/DerivedData
else
echo "Stable LeapSDK pin; SPM caches left in place"
fi
- name: Generate Xcode project
run: make setup
- name: Build for iOS Simulator
# Pin to a concrete simulator destination so xcodebuild picks the right arch on
# the runner (Apple Silicon today; Intel-hostile to EXCLUDED_ARCHS=x86_64). Matches
# what `make build` runs locally.
run: |
xcodebuild -project ${{ matrix.demo }}.xcodeproj \
-scheme ${{ matrix.demo }} \
-destination 'platform=iOS Simulator,name=iPhone 17' \
build
111 changes: 111 additions & 0 deletions .github/workflows/jvm-leap-chat-cli-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: JVM LeapChatCli Build
on:
push:
branches: [ main ]
paths:
- 'JVM/LeapChatCli/**'
- '.github/workflows/jvm-leap-chat-cli-test.yml'
pull_request:
branches: [ main ]
paths:
- 'JVM/LeapChatCli/**'
- '.github/workflows/jvm-leap-chat-cli-test.yml'
workflow_dispatch:

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

# Least-privilege default. The workflow only consumes Maven Central / model
# downloads — no GitHub API access needed.
permissions:
contents: read

jobs:
build-and-smoke-test:
runs-on: ubuntu-24.04
timeout-minutes: 20
defaults:
run:
working-directory: JVM/LeapChatCli
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Cache downloaded model
uses: actions/cache@v5
with:
path: JVM/LeapChatCli/leap_models
key: leap-models-LFM2.5-350M-Q4_0-v1
- name: Purge cached leap-sdk artifacts if Sonatype snapshot was republished
# The pin may be a SNAPSHOT in which case the upstream may force-update it under
# the same SNAPSHOT version, with new contents. When that happens,
# `--refresh-dependencies` alone isn't enough: the gradle cache restored by
# setup-java's cache: 'gradle' still contains the prior artifact, and gradle
# short-circuits the re-download because the cached SHA matches its metadata.
#
# Detect the situation by comparing Sonatype's <lastUpdated> for the snapshot
# against a marker we wrote on the previous run. Purge only when they differ —
# so the cache stays warm in the common case (no upstream republish) and
# rebuilds get the new artifact in the rare case (snapshot updated).
run: |
SDK_VERSION=$(grep -E '^\s*leapSdk\s*=' gradle/libs.versions.toml | sed -E 's/.*"([^"]+)".*/\1/')
# Whitelist the parsed version before interpolating into the curl URL
# below. The value comes from a checked-in TOML so it isn't directly
# attacker-controlled, but a typo or bad PR could let a stray char
# reach `curl` — semver + optional pre-release + optional -SNAPSHOT
# is the only shape we expect.
if ! echo "$SDK_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)*(-[A-Za-z0-9.]+)?(-SNAPSHOT)?$'; then
echo "(unexpected leapSdk version shape '$SDK_VERSION' — skipping purge check)"
exit 0
fi
case "$SDK_VERSION" in
*-SNAPSHOT) ;;
*) echo "leap-sdk pinned to stable $SDK_VERSION; skipping snapshot purge check"; exit 0 ;;
esac
# Check the parent `leap-sdk` artifact's <lastUpdated> — when the SDK is
# republished, the parent and every per-target sibling get re-uploaded
# together with the same timestamp, so one URL is representative for any
# workflow. Marker lives under ~/.gradle/caches so it's preserved by
# setup-java's gradle cache between runs.
REMOTE_TS=$(curl -fsSL "https://central.sonatype.com/repository/maven-snapshots/ai/liquid/leap/leap-sdk/$SDK_VERSION/maven-metadata.xml" 2>/dev/null \
| grep -oE '<lastUpdated>[0-9]+</lastUpdated>' | grep -oE '[0-9]+' || true)
MARKER=~/.gradle/caches/leap-snapshot-marker
LOCAL_TS=$(cat "$MARKER" 2>/dev/null || true)
if [ -z "$REMOTE_TS" ]; then
echo "(could not fetch snapshot metadata for $SDK_VERSION from Sonatype — skipping purge check)"
elif [ "$REMOTE_TS" = "$LOCAL_TS" ]; then
echo "Sonatype snapshot timestamp unchanged for $SDK_VERSION ($LOCAL_TS); leap-sdk cache is fresh"
else
echo "Sonatype snapshot timestamp changed for $SDK_VERSION (${LOCAL_TS:-<none>} -> $REMOTE_TS); purging leap-sdk cache"
rm -rf ~/.gradle/caches/modules-2/files-2.1/ai.liquid.leap* \
~/.gradle/caches/modules-2/metadata-*/descriptors/ai.liquid.leap*
mkdir -p "$(dirname "$MARKER")"
echo "$REMOTE_TS" > "$MARKER"
fi
- name: Build runnable distribution
run: ./gradlew --refresh-dependencies installDist
- name: Smoke test (download model, load, :quit)
run: |
# Pipe :quit so the JVM exits as soon as the model is loaded.
# Verifies the full LeapDownloader → loadModel → createConversation
# flow: JNI extraction + dlopen of all bundled .so files + the
# CPU-dispatched llamacpp backend init picks the right
# libggml-cpu-<arch>.so for the runner's CPU.
set +e
printf ':quit\n' | timeout 900 build/install/leap-chat-cli/bin/leap-chat-cli \
> stdout.log 2> stderr.log
exit_code=$?
set -e
echo "--- exit code: $exit_code ---"
echo "--- stdout ---"; cat stdout.log
echo "--- stderr ---"; cat stderr.log
test "$exit_code" -eq 0
grep -q 'ready (model id:' stdout.log
Loading
Loading