vpnhide/.github/workflows/ci.yml
okhsunrog 0d4cf09866 chore: CI + scripts cleanup (review items #12 #13 #24 #31 #32 #37)
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.
2026-04-27 01:14:03 +03:00

377 lines
13 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
# `--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.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 }}