vpnhide/.github/workflows/ci.yml
okhsunrog 5350f8e2f6 ci: shave another ~80s off lint/lsposed jobs
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%)
2026-04-27 00:28:25 +03:00

345 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.52 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 }}