BigMoeOnEdge/.github/workflows/ci.yml
Helldez 8c8c8f1840
ci: pin third-party actions to commit SHAs (#160)
nttld/setup-ndk and softprops/action-gh-release ran from mutable major
tags inside the APK job, which holds a contents:write token. Whoever
controls those tags upstream could have pointed them at unreviewed code
with permission to rewrite this repo's release assets.

Both now resolve to a fixed commit, with the human-readable version in a
trailing comment. GitHub-owned actions stay on major tags: pinning them
would cost an SHA bump on every upstream release for a materially
smaller risk.

The signing keystore was never reachable this way. It lives only in
release-apk.yml, which runs no third-party action and triggers only on
events that already require write access.
2026-08-04 12:19:04 +02:00

177 lines
7.7 KiB
YAML

name: ci
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
paths-ignore: [ '**/*.md', 'docs/**', 'LICENSE', '.gitignore' ]
workflow_dispatch: {}
# A newer push to the same ref cancels the older, still-running CI — so a burst of commits
# spends minutes only on the last one instead of building every intermediate commit.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# Does this push carry anything a build could break?
#
# `paths-ignore` handles that for pull requests, but it cannot be put on the push trigger: the
# same trigger carries the release tags, and a tag push has no commit range for a path filter to
# read, so filtering here would silence the tag build that attaches the APK. Hence the decision
# is made once, in ten seconds, and each build job is gated on it.
#
# Anything not a push (pull_request, workflow_dispatch) and every tag build answers "yes"
# unconditionally — this is a saver for docs merges, not a second opinion on what to test.
changes:
runs-on: ubuntu-latest
outputs:
code: ${{ steps.decide.outputs.code }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- id: decide
env:
EVENT: ${{ github.event_name }}
REF: ${{ github.ref }}
BEFORE: ${{ github.event.before }}
SHA: ${{ github.sha }}
run: |
say() { echo "code=$1" >> "$GITHUB_OUTPUT"; echo "code=$1"; }
case "$EVENT:$REF" in
push:refs/tags/*) say true; exit 0 ;;
push:*) ;;
*) say true; exit 0 ;;
esac
# A branch created by this push, or a force-push past the old head, leaves nothing to
# diff against; build rather than guess.
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ] \
|| ! git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then
say true; exit 0
fi
files=$(git diff --name-only "$BEFORE" "$SHA")
echo "changed:"; echo "$files"
if echo "$files" | grep -qvE '(\.md$|^docs/|^LICENSE$|^\.gitignore$)'; then
say true
else
say false
fi
format:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: clang-format (our sources only)
# Pinned to 18 because AGENTS.md tells contributors to match that version locally. Plain
# `clang-format` is whatever the runner image ships, which happens to be 18 today: an image
# bump would silently start failing every PR against a version nobody was told about.
run: |
sudo apt-get update && sudo apt-get install -y clang-format-18
find core cli tests -type f \( -name '*.cpp' -o -name '*.h' \) \
-print0 | xargs -0 clang-format-18 --dry-run --Werror
host-linux:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Install deps
run: |
sudo apt-get update && sudo apt-get install -y cmake ninja-build
python3 -m pip install --upgrade pip
python3 -m pip install gguf numpy
- name: Configure & build
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
- name: Byte-identity gates
run: cd build && ctest --output-on-failure
host-windows:
# Windows minutes bill at 2x. It only compile-checks the Win32 I/O paths, so run it where it
# matters — PRs (before merge) and release tags — not on every main push (already tested on PR).
if: github.event_name != 'push' || startsWith(github.ref, 'refs/tags/v')
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Configure & build (compile-check the Win32 I/O paths)
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBMOE_BUILD_TESTS=OFF
cmake --build build --config Release -j
# Cross-compiles the arm64 engine (the native compile gate) and packages the example
# APK around it, uploading the APK as an artifact — and attaching it to the release on a
# tag push. Debug-signed: for on-device validation, not Play distribution.
android-apk:
# The long job (NDK + cross-compile + gradle). Build it on PRs (catch APK breakage before merge)
# and on release tags (attach the APK), but not on every main push. Trigger a one-off manually
# with the Run workflow button (workflow_dispatch) if you need a fresh main APK between releases.
if: github.event_name != 'push' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write # only used by the tag-triggered release attach step
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Set up NDK
# Third-party action, pinned by commit SHA: a mutable tag here would run whatever the
# upstream repo points it at, inside a job that holds a contents:write token.
uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: ndk
with:
# r27c == 27.2.12479018, the NDK release-apk.yml builds published APKs with. These two
# must not drift, or CI validates a build nobody installs.
ndk-version: r27c
- name: Cross-compile bmoe-cli (arm64)
env:
ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
run: |
cmake -S . -B build-android -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 \
-DCMAKE_BUILD_TYPE=Release -DBMOE_BUILD_TESTS=OFF \
-DGGML_NATIVE=OFF -DGGML_OPENMP=OFF -DGGML_OPENCL=OFF \
-DGGML_CPU_ARM_ARCH="armv8.2-a+dotprod+fp16" \
-DLLAMA_CURL=OFF
cmake --build build-android -j
- name: Stage engine binaries into jniLibs
env:
ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
run: |
jni=examples/android/app/src/main/jniLibs/arm64-v8a
mkdir -p "$jni"
cp build-android/cli/bmoe-cli "$jni/libbmoe-cli.so"
find build-android \( -name 'libggml*.so' -o -name 'libllama*.so' \) -exec cp {} "$jni/" \;
# bmoe-cli links the c++_shared STL; ship its runtime from the NDK sysroot.
cp "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so" "$jni/"
ls -la "$jni"
- name: Build debug APK (dev flavor — sideloaded, all-files access)
working-directory: examples/android
run: ./gradlew assembleDevDebug --no-daemon
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: bmoe-example-dev-debug-apk
path: examples/android/app/build/outputs/apk/dev/debug/app-dev-debug.apk
if-no-files-found: error
- name: Attach APK to release
if: startsWith(github.ref, 'refs/tags/v')
# Pinned by commit SHA for the same reason. This step only runs on a tag push, so a
# wrong pin would not surface until a release: resolve the SHA, do not hand-edit it.
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: examples/android/app/build/outputs/apk/dev/debug/app-dev-debug.apk