mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-05-02 00:22:14 +00:00
Six small review-list items rolled together — all CI/dev-tooling, no runtime behaviour change. #12 Dockerfile: pin Rust 1.95.0 and cargo-ndk 4.1.2 (was floating `stable` + latest cargo-ndk on monthly rebuild). Versions live in ENV vars to make the next bump a one-line edit. #13 Add shellcheck to lint job. SC2034/SC3043 excluded — Magisk reads SKIPUNZIP externally; Android's /system/bin/sh (mksh on Pixel) does support `local` despite POSIX. Verified locally that the 11 .sh files (module-side + dev tooling) pass. shellcheck baked into the CI image via apt; inline apt-get fallback covers the window before image rebuild. #24 ci.yml keystore.properties: replace heredoc with `printf '%s\n'`. Heredoc without single-quoted EOF re-expands $, backticks and backslashes in the password — printf takes the value verbatim. #31 scripts/release.py::patch_file now hard-fails when a regex pattern doesn't match (was silently leaving stale versions). #32 Split rotate_fragments_into_history into rotate + delete steps so release.py can save_json + write_md *before* unlinking the fragment files. If anything in between fails, fragments are still on disk and the run is retryable. #37 codegen-interfaces.py: emit `assert!(matches_vpn(…), msg)` / `assert!(!matches_vpn(…), msg)` instead of `assert_eq!(matches_vpn(…), true/false, msg)` — clippy::bool_assert_comparison was firing on every generated row under `cargo clippy --tests`. Both generated test modules regenerated. CI's clippy steps now also pass `--tests` so this class of regression is caught.
377 lines
13 KiB
YAML
377 lines
13 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
|
||
# `--tests` so generated test modules are also linted
|
||
# (catches `bool_assert_comparison`-style regressions in
|
||
# codegen output).
|
||
- name: clippy (zygisk)
|
||
run: cd zygisk && cargo ndk -t arm64-v8a clippy --tests -- -D warnings
|
||
- name: clippy (lsposed native)
|
||
run: cd lsposed/native && cargo ndk -t arm64-v8a clippy --tests -- -D warnings
|
||
- name: cargo test (zygisk)
|
||
run: cd zygisk && cargo test
|
||
- name: cargo test (lsposed native)
|
||
run: cd lsposed/native && cargo test
|
||
|
||
# Shell — module-side scripts (Magisk/KSU) and host-side dev tooling.
|
||
# shellcheck is preinstalled in the CI image (apt). The list is
|
||
# explicit so we don't accidentally pick up vendored .sh from
|
||
# zygisk/third_party.
|
||
- name: shellcheck
|
||
# SC2034: `SKIPUNZIP` and the `for i in seq …` counters look unused
|
||
# to shellcheck — Magisk reads SKIPUNZIP externally, the
|
||
# counters are loop iterators we don't read by name.
|
||
# SC3043: `local` is "not POSIX" but Android's /system/bin/sh
|
||
# (mksh on Pixel) supports it, and our module-side
|
||
# scripts always run there.
|
||
# The inline `apt-get install` is a one-job fallback for the
|
||
# window between this PR landing and the next ci-image rebuild
|
||
# (the Dockerfile bake also adds shellcheck). After the rebuild
|
||
# this no-ops in <1s.
|
||
run: |
|
||
if ! command -v shellcheck >/dev/null; then
|
||
apt-get update -qq && apt-get install -y --no-install-recommends shellcheck >/dev/null
|
||
fi
|
||
shellcheck -x -e SC2034,SC3043 \
|
||
kmod/module/customize.sh kmod/module/post-fs-data.sh kmod/module/service.sh \
|
||
zygisk/module/customize.sh zygisk/module/service.sh \
|
||
portshide/module/customize.sh portshide/module/service.sh \
|
||
portshide/module/uninstall.sh portshide/module/vpnhide_ports_apply.sh \
|
||
scripts/clean-device.sh scripts/update-json.sh
|
||
|
||
# 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
|
||
# Build via printf — `%s` swallows the value verbatim with no
|
||
# shell expansion of $, backticks, backslashes, or `!`. Heredoc
|
||
# without single-quoted EOF would re-expand each line.
|
||
{
|
||
printf 'password=%s\n' "$KEYSTORE_PASSWORD"
|
||
printf 'keyAlias=%s\n' "$KEY_ALIAS"
|
||
printf 'storeFile=%s\n' "$KEYSTORE_PATH"
|
||
} > "$GITHUB_WORKSPACE/lsposed/keystore.properties"
|
||
|
||
# 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 }}
|