mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-04-28 06:31:27 +00:00
Repo had ~1800 lines of Python (kmod/build.py, scripts/*, zygisk/build.py,
portshide/build-zip.py) with no formatter or linter. Long-lived scripts
like scripts/release.py and scripts/codegen-interfaces.py benefit from
catching unused-import / undefined-name / outdated-syntax issues early.
pyproject.toml — ruff config, target-py312, line-length 100,
rules E F W I B UP SIM. Excludes zygisk/third_party,
target/, .claude/.
ci.yml — astral-sh/ruff-action@v4 for `format --check` and `check`,
ahead of the slow Rust/Gradle steps so it fails fast.
docs/development.md — add `uvx ruff …` to the local-lint snippet.
Cleanup applied (`ruff format` + `ruff check --fix`):
- reformat: kmod/build.py, scripts/{changelog_lib,codegen-interfaces,
release,stats}.py, zygisk/build.py
- I001: split multi-name imports onto separate lines after the
sys.path.insert prelude (kmod/build.py, zygisk/build.py)
- E501 manual: wrap one console.print line in scripts/release.py
Stdlib-only invariant from scripts/build_lib.py is preserved — ruff is
a dev/CI tool, not imported at runtime.
338 lines
11 KiB
YAML
338 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
|
||
with:
|
||
args: format --check
|
||
- name: ruff check
|
||
uses: astral-sh/ruff-action@v4
|
||
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
|
||
run: cd lsposed && ./gradlew :app:lint :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).
|
||
- name: Build APK
|
||
run: |
|
||
cd "$GITHUB_WORKSPACE/lsposed"
|
||
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
||
./gradlew assembleRelease
|
||
cp app/build/outputs/apk/release/app-release.apk "$GITHUB_WORKSPACE/vpnhide.apk"
|
||
else
|
||
./gradlew assembleDebug
|
||
cp app/build/outputs/apk/debug/app-debug.apk "$GITHUB_WORKSPACE/vpnhide.apk"
|
||
fi
|
||
|
||
- 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 }}
|