mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-04-28 06:31:27 +00:00
Profiling the warm-cache run on PR #105 showed three remaining hot spots in the Gradle phase: installUniffiBindgen 52s ← cargo install on every CI build cargoBuildAndroidArm64Debug 30s ← Rust crate compile lintAnalyze* (3 variants) 43s ← AGP Lint × main + unit + androidTest This PR cuts the first one entirely and trims the third. - Dockerfile: pre-install uniffi-bindgen 0.29.x in the CI image so Gobley's :app:installUniffiBindgen task finds it ready instead of rebuilding it from sources on every run. Triggers a ci-image rebuild on merge — wait for that workflow to finish before merging consumers (or the first lint/lsposed run will still hit the old image and behave as before). - lsposed/gradle.properties: enable build cache + configuration cache. Verified locally: `./gradlew :app:assembleDebug --configuration-cache` reports "Configuration cache entry stored" cleanly with Gobley 0.3.7 + AGP 8.9.3 + Kotlin 2.1.20. - lsposed/app/build.gradle.kts: `lint { checkTestSources = false }`. Skips lintAnalyzeDebugUnitTest / lintAnalyzeDebugAndroidTest. Test sources here are pure JVM unit-test logic — functional bugs caught by :app:testDebugUnitTest, no Android-lifecycle code to lint. Deliberately leave `checkReleaseBuilds` at its default so ad-hoc `./gradlew :app:lint` still catches R8/ProGuard issues. - .github/workflows/ci.yml: `:app:lint` -> `:app:lintDebug`. Lints the debug variant only on PRs; release-variant Lint stays available locally / for future tag-time CI. - docs/development.md: refresh local-lint snippet. Expected effect on warm cache (cumulative on top of PR #105): lint 286s -> ~190s (3m10s, -32%) lsposed 227s -> ~130s (2m10s, -42%)
345 lines
11 KiB
YAML
345 lines
11 KiB
YAML
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
tags: ['v*']
|
||
pull_request:
|
||
workflow_dispatch:
|
||
|
||
permissions:
|
||
contents: read
|
||
packages: read
|
||
|
||
jobs:
|
||
setup:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
image: ${{ steps.img.outputs.image }}
|
||
steps:
|
||
- id: img
|
||
env:
|
||
REPO: ${{ github.repository }}
|
||
run: echo "image=ghcr.io/${REPO,,}/ci:latest" >> "$GITHUB_OUTPUT"
|
||
|
||
lint:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
submodules: recursive
|
||
fetch-depth: 0
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
# Python lint + format (ruff). Fast (~100 ms on 1800 LoC) so it
|
||
# runs first — fails before the slow Rust/Gradle steps.
|
||
- name: ruff format
|
||
uses: astral-sh/ruff-action@v4.0.0
|
||
with:
|
||
args: format --check
|
||
- name: ruff check
|
||
uses: astral-sh/ruff-action@v4.0.0
|
||
with:
|
||
args: check
|
||
|
||
# Cache cargo deps + target dirs for clippy/test. Same key shape as
|
||
# zygisk + lsposed jobs — when those run on the same Cargo.lock the
|
||
# restore-keys fallback shares warm artifacts across jobs.
|
||
- name: Cache cargo
|
||
uses: actions/cache@v5
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
zygisk/target
|
||
lsposed/native/target
|
||
key: cargo-${{ runner.os }}-lint-${{ hashFiles('zygisk/Cargo.lock', 'lsposed/native/Cargo.lock') }}
|
||
restore-keys: cargo-${{ runner.os }}-lint-
|
||
|
||
# Gradle cache (deps + configuration cache + wrapper). cache-read-only
|
||
# on PRs so only main pushes write — keeps the cache from churning on
|
||
# every PR's branch-scoped key.
|
||
- name: Set up Gradle
|
||
uses: gradle/actions/setup-gradle@v6
|
||
with:
|
||
cache-read-only: ${{ github.event_name == 'pull_request' }}
|
||
|
||
# Codegen
|
||
- name: Verify generated iface lists are up to date
|
||
run: |
|
||
python3 scripts/codegen-interfaces.py
|
||
if ! git diff --quiet; then
|
||
echo "::error::data/interfaces.toml is out of sync with generated files. Run scripts/codegen-interfaces.py and commit the result." >&2
|
||
git --no-pager diff
|
||
exit 1
|
||
fi
|
||
|
||
# Rust
|
||
- name: rustfmt
|
||
run: |
|
||
cd zygisk && cargo fmt --check
|
||
cd ../lsposed/native && cargo fmt --check
|
||
- name: clippy (zygisk)
|
||
run: cd zygisk && cargo ndk -t arm64-v8a clippy -- -D warnings
|
||
- name: clippy (lsposed native)
|
||
run: cd lsposed/native && cargo ndk -t arm64-v8a clippy -- -D warnings
|
||
- name: cargo test (zygisk)
|
||
run: cd zygisk && cargo test
|
||
- name: cargo test (lsposed native)
|
||
run: cd lsposed/native && cargo test
|
||
|
||
# C (kernel module)
|
||
- name: clang-format
|
||
run: clang-format --dry-run --Werror kmod/vpnhide_kmod.c
|
||
- name: kmod iface-list test (host build)
|
||
run: |
|
||
cd kmod
|
||
gcc -O2 -Wall -Werror -o /tmp/test_iface_lists test_iface_lists.c
|
||
/tmp/test_iface_lists
|
||
|
||
# Kotlin
|
||
- name: ktlint
|
||
run: ktlint "lsposed/**/*.kt"
|
||
# Single Gradle invocation: lint + tests share one configuration
|
||
# phase + warm daemon. Configures Gobley's cargo plugin once instead
|
||
# of twice. ANDROID_NDK_ROOT is baked into the CI image
|
||
# (Dockerfile ENV), no manual export needed.
|
||
- name: Android lint + Kotlin unit tests
|
||
# `:app:lintDebug` (not `:app:lint`) runs Lint on the debug
|
||
# variant only — release-variant Lint covers R8/ProGuard issues
|
||
# and is reserved for ad-hoc local runs / future tag CI.
|
||
run: cd lsposed && ./gradlew :app:lintDebug :app:testDebugUnitTest
|
||
|
||
kmod:
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
matrix:
|
||
kmi:
|
||
- android12-5.10
|
||
- android13-5.10
|
||
- android13-5.15
|
||
- android14-5.15
|
||
- android14-6.1
|
||
- android15-6.6
|
||
- android16-6.12
|
||
# Tag here mirrors `DDK_IMAGE_TAG` in kmod/build.py — bump both
|
||
# together so local builds and CI use the exact same image.
|
||
container:
|
||
image: ghcr.io/ylarod/ddk-min:${{ matrix.kmi }}-20260313
|
||
env:
|
||
KMI: ${{ matrix.kmi }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Build and package kernel module
|
||
run: python3 kmod/build.py --kmi $KMI --inside-container
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: vpnhide-kmod-${{ matrix.kmi }}
|
||
path: vpnhide-kmod-${{ matrix.kmi }}.zip
|
||
if-no-files-found: error
|
||
|
||
zygisk:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
submodules: recursive
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Cache cargo
|
||
uses: actions/cache@v5
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
zygisk/target
|
||
key: cargo-${{ runner.os }}-${{ hashFiles('zygisk/Cargo.lock') }}
|
||
restore-keys: cargo-${{ runner.os }}-
|
||
|
||
- name: Build module zip
|
||
env:
|
||
UPDATE_JSON_URL: https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-zygisk.json
|
||
run: |
|
||
cd zygisk
|
||
python3 ./build.py
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: vpnhide-zygisk
|
||
path: zygisk/target/vpnhide-zygisk.zip
|
||
if-no-files-found: error
|
||
|
||
lsposed:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Cache cargo
|
||
uses: actions/cache@v5
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
lsposed/native/target
|
||
key: cargo-${{ runner.os }}-lsposed-${{ hashFiles('lsposed/native/Cargo.lock') }}
|
||
restore-keys: cargo-${{ runner.os }}-lsposed-
|
||
|
||
# Gradle cache (deps + configuration cache + wrapper). cache-read-only
|
||
# on PRs so only main pushes write the cache.
|
||
- name: Set up Gradle
|
||
uses: gradle/actions/setup-gradle@v6
|
||
with:
|
||
cache-read-only: ${{ github.event_name == 'pull_request' }}
|
||
|
||
- name: Set up keystore
|
||
env:
|
||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
run: |
|
||
KEYSTORE_PATH="$GITHUB_WORKSPACE/lsposed/release.jks"
|
||
if [ -n "$KEYSTORE_BASE64" ]; then
|
||
echo "$KEYSTORE_BASE64" | base64 --decode > "$KEYSTORE_PATH"
|
||
else
|
||
echo "ANDROID_KEYSTORE_BASE64 is empty (fork PR); generating an ephemeral keystore. Resulting APK is signed with a throwaway key and is NOT suitable for release."
|
||
KEYSTORE_PASSWORD=ephemeral
|
||
KEY_ALIAS=ephemeral
|
||
keytool -genkeypair -v \
|
||
-keystore "$KEYSTORE_PATH" \
|
||
-storepass "$KEYSTORE_PASSWORD" \
|
||
-keypass "$KEYSTORE_PASSWORD" \
|
||
-alias "$KEY_ALIAS" \
|
||
-keyalg RSA -keysize 4096 -validity 365 \
|
||
-dname "CN=vpnhide-fork-ci, O=vpnhide, C=US"
|
||
fi
|
||
cat > "$GITHUB_WORKSPACE/lsposed/keystore.properties" <<EOF
|
||
password=$KEYSTORE_PASSWORD
|
||
keyAlias=$KEY_ALIAS
|
||
storeFile=$KEYSTORE_PATH
|
||
EOF
|
||
|
||
# Release tags get the full assembleRelease (R8/ProGuard, signed APK
|
||
# ready for the GitHub release). PRs and main pushes get assembleDebug
|
||
# — same code paths exercised, no R8 step (~1.5–2 min faster).
|
||
# `case` instead of `[[`: container jobs default to /bin/sh (POSIX).
|
||
- name: Build APK
|
||
run: |
|
||
cd "$GITHUB_WORKSPACE/lsposed"
|
||
case "$GITHUB_REF" in
|
||
refs/tags/v*)
|
||
./gradlew assembleRelease
|
||
cp app/build/outputs/apk/release/app-release.apk "$GITHUB_WORKSPACE/vpnhide.apk"
|
||
;;
|
||
*)
|
||
./gradlew assembleDebug
|
||
cp app/build/outputs/apk/debug/app-debug.apk "$GITHUB_WORKSPACE/vpnhide.apk"
|
||
;;
|
||
esac
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: vpnhide-apk
|
||
path: vpnhide.apk
|
||
if-no-files-found: error
|
||
|
||
portshide:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Package ports module zip
|
||
env:
|
||
UPDATE_JSON_URL: https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-ports.json
|
||
run: |
|
||
cd portshide
|
||
python3 ./build-zip.py
|
||
mv vpnhide-ports.zip "$GITHUB_WORKSPACE/vpnhide-ports.zip"
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: vpnhide-ports
|
||
path: vpnhide-ports.zip
|
||
if-no-files-found: error
|
||
|
||
release:
|
||
needs: [kmod, zygisk, lsposed, portshide]
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
runs-on: ubuntu-latest
|
||
# Only the release job needs write — used by softprops/action-gh-release
|
||
# below to create the draft GitHub release. lint/build jobs run on the
|
||
# workflow-level `contents: read`.
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Download all artifacts
|
||
uses: actions/download-artifact@v8
|
||
with:
|
||
path: dist/
|
||
merge-multiple: true
|
||
|
||
- name: Extract release notes from CHANGELOG.md
|
||
run: |
|
||
TAG="${{ github.ref_name }}"
|
||
awk -v t="^## ${TAG}\$" '$0~t{flag=1;next} /^## v/{flag=0} flag' \
|
||
CHANGELOG.md > release-notes.md
|
||
echo "=== release-notes.md ==="
|
||
cat release-notes.md
|
||
|
||
- name: Create draft release
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
tag_name: ${{ github.ref_name }}
|
||
body_path: release-notes.md
|
||
generate_release_notes: true
|
||
draft: true
|
||
files: |
|
||
dist/*.zip
|
||
dist/*.apk
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|