vpnhide/.github/workflows/ci.yml
okhsunrog 91013acb54 ci+chore: add ruff (format + lint) for python scripts
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.
2026-04-26 23:48:37 +03:00

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