mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-07-24 16:33:55 +00:00
720 lines
26 KiB
YAML
720 lines
26 KiB
YAML
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
tags: ['v*']
|
||
pull_request:
|
||
workflow_dispatch:
|
||
|
||
permissions:
|
||
contents: read
|
||
packages: read
|
||
|
||
env:
|
||
RUFF_VERSION: "0.15.20"
|
||
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||
|
||
jobs:
|
||
setup:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
image: ${{ steps.img.outputs.image }}
|
||
ddkqemu: ${{ steps.img.outputs.ddkqemu }}
|
||
kpmlegacy: ${{ steps.img.outputs.kpmlegacy }}
|
||
steps:
|
||
- id: img
|
||
env:
|
||
REPO: ${{ github.repository }}
|
||
run: |
|
||
echo "image=ghcr.io/${REPO,,}/ci:latest" >> "$GITHUB_OUTPUT"
|
||
echo "ddkqemu=ghcr.io/${REPO,,}/ddk-qemu" >> "$GITHUB_OUTPUT"
|
||
echo "kpmlegacy=ghcr.io/${REPO,,}/kpm-qemu-legacy:latest" >> "$GITHUB_OUTPUT"
|
||
|
||
# Build the raw-syscall probe from the PR checkout once, then feed that exact
|
||
# binary to every QEMU job. The long-lived QEMU images also carry a fallback
|
||
# copy, but relying on it makes a PR that adds/changes a probe fail until the
|
||
# post-merge image rebuild catches up.
|
||
qemu-native-probes:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
|
||
- name: Build state-aware socket bind probe
|
||
run: |
|
||
TC="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||
mkdir -p .artifacts/qemu-native-probes
|
||
"$TC/aarch64-linux-android24-clang" -static -O2 -Wall -Wextra -Werror \
|
||
-o .artifacts/qemu-native-probes/bind-probe kmod/test/bind-probe.c
|
||
"$TC/llvm-strip" .artifacts/qemu-native-probes/bind-probe
|
||
|
||
- name: Upload QEMU native probes
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: qemu-native-probes
|
||
path: .artifacts/qemu-native-probes
|
||
if-no-files-found: error
|
||
|
||
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@v7
|
||
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:
|
||
version: ${{ env.RUFF_VERSION }}
|
||
args: format --check
|
||
- name: ruff check
|
||
uses: astral-sh/ruff-action@v4.0.0
|
||
with:
|
||
version: ${{ env.RUFF_VERSION }}
|
||
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@v6
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
target
|
||
zygisk/target
|
||
lsposed/native/target
|
||
key: cargo-${{ runner.os }}-lint-${{ hashFiles('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 — regenerate from the data/*.toml sources and fail on drift.
|
||
- name: Verify generated files are up to date (iface lists + hook registry)
|
||
run: |
|
||
python3 scripts/codegen-interfaces.py
|
||
python3 scripts/codegen-hooks.py
|
||
if ! git diff --quiet; then
|
||
echo "::error::a data/*.toml is out of sync with generated files. Run scripts/codegen-interfaces.py and scripts/codegen-hooks.py and commit the result." >&2
|
||
git --no-pager diff
|
||
exit 1
|
||
fi
|
||
|
||
# Rust
|
||
- name: rustfmt
|
||
run: |
|
||
cargo fmt --check
|
||
cd lsposed/native && cargo fmt --check
|
||
- name: clippy (protocol + activator)
|
||
run: cargo clippy -p vpnhide_protocol -p vpnhide_protocol_diff -p vpnhide_activator --all-targets -- -D warnings
|
||
# `--tests` so generated test modules are also linted
|
||
# (catches `bool_assert_comparison`-style regressions in
|
||
# codegen output).
|
||
- name: clippy (zygisk)
|
||
run: cargo ndk -t arm64-v8a clippy -p vpnhide_zygisk --tests -- -D warnings
|
||
- name: clippy (lsposed native)
|
||
run: cd lsposed/native && cargo ndk -t arm64-v8a clippy --tests -- -D warnings
|
||
- name: cargo test (workspace)
|
||
run: cargo test -p vpnhide_protocol -p vpnhide_protocol_diff -p vpnhide_activator -p vpnhide_zygisk
|
||
- 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 kmod/module/uninstall.sh \
|
||
kmod/kpm/module/customize.sh kmod/kpm/module/post-fs-data.sh kmod/kpm/module/service.sh kmod/kpm/module/uninstall.sh \
|
||
zygisk/module/customize.sh zygisk/module/service.sh zygisk/module/uninstall.sh \
|
||
portshide/module/customize.sh portshide/module/service.sh \
|
||
portshide/module/uninstall.sh \
|
||
scripts/clean-device.sh scripts/update-json.sh \
|
||
kmod/test/run.sh kmod/test/build-kernel.sh kmod/test/init.sh \
|
||
kmod/test/run-kpm.sh kmod/test/init-kpm.sh kmod/test/build-source-kernel.sh
|
||
|
||
# C (kernel module)
|
||
- name: clang-format
|
||
run: scripts/clang-format-c.sh --check
|
||
- 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
|
||
# Shared backend-agnostic logic + the control/stats protocol parser /
|
||
# serialiser. The protocol harness runs the language-independent golden
|
||
# vectors (kmod/shared/protocol_vectors.tsv) — the C side of the
|
||
# cross-language parity check (docs/protocol.md §8).
|
||
- name: shared-logic + protocol host tests
|
||
run: |
|
||
cd kmod/shared
|
||
gcc -O2 -Wall -Wextra -Werror -I.. -o /tmp/test_vpnhide_logic test_vpnhide_logic.c
|
||
/tmp/test_vpnhide_logic
|
||
gcc -O2 -Wall -Wextra -Werror -I.. -o /tmp/test_protocol test_protocol.c
|
||
/tmp/test_protocol protocol_vectors.tsv
|
||
|
||
# Kotlin
|
||
# ktlint = formatting/style; detekt = complexity/length/dead-code/bug
|
||
# patterns; cpdCheck = cross-file copy-paste. detekt runs clean with no
|
||
# baseline (inherent spots are @Suppress-ed at the call site); cpdCheck
|
||
# is clean at its 100-token threshold. See lsposed/config/detekt/ and
|
||
# the cpd block in lsposed/build.gradle.kts.
|
||
- name: ktlint
|
||
run: ktlint "lsposed/app/src/**/*.kt"
|
||
# Single Gradle invocation: detekt + cpd + lint + tests share one
|
||
# configuration phase + warm daemon. Configures Gobley's cargo plugin
|
||
# once instead of four times. ANDROID_NDK_ROOT is baked into the CI
|
||
# image (Dockerfile ENV), no manual export needed.
|
||
- name: detekt + CPD + 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:detekt cpdCheck :app:lintDebug :app:testDebugUnitTest
|
||
|
||
kmod-activator:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Cache cargo
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
target
|
||
key: cargo-${{ runner.os }}-kmod-activator-${{ hashFiles('Cargo.lock') }}
|
||
restore-keys: cargo-${{ runner.os }}-kmod-activator-
|
||
|
||
- name: Build kmod activator
|
||
run: cargo ndk -t arm64-v8a build --release -p vpnhide_activator --bin kmod
|
||
|
||
- name: Upload kmod activator
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: kmod-activator
|
||
path: target/aarch64-linux-android/release/kmod
|
||
if-no-files-found: error
|
||
|
||
kmod:
|
||
needs: kmod-activator
|
||
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@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Download kmod activator
|
||
uses: actions/download-artifact@v7
|
||
with:
|
||
name: kmod-activator
|
||
path: .artifacts/kmod-activator
|
||
|
||
- name: Build and package kernel module
|
||
env:
|
||
VPNHIDE_KMOD_ACTIVATOR: ${{ github.workspace }}/.artifacts/kmod-activator/kmod
|
||
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
|
||
|
||
# Runtime gate: boot a real GKI kernel for each KMI in QEMU and assert the
|
||
# module actually hides VPN state (per-vector A/B), not just that it compiled.
|
||
# Catches register/inlining bugs that source + build checks can't (e.g. the
|
||
# static-function rt_fill_info register issue). Runs in the per-KMI ddk-qemu
|
||
# image (built by qemu-image.yml) which bakes a bootable kernel + its build
|
||
# tree; the module is built here against that tree so it matches the kernel.
|
||
# QEMU runs under TCG (no KVM on hosted runners) — slow but fine. See kmod/test/.
|
||
kmod-qemu:
|
||
needs: [setup, qemu-native-probes]
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
kmi:
|
||
- android12-5.10
|
||
- android13-5.10
|
||
- android13-5.15
|
||
- android14-5.15
|
||
- android14-6.1
|
||
- android15-6.6
|
||
- android16-6.12
|
||
container:
|
||
# Lowercased base from setup (GHCR names must be lowercase); credentials
|
||
# because it's a repo-owned (private) package, like the other jobs.
|
||
image: ${{ needs.setup.outputs.ddkqemu }}:${{ matrix.kmi }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
env:
|
||
KMI: ${{ matrix.kmi }}
|
||
VPNHIDE_BIND_REQUIRED: "1"
|
||
VPNHIDE_BIND_BIN: ${{ github.workspace }}/.artifacts/qemu-native-probes/bind-probe
|
||
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Download QEMU native probes
|
||
uses: actions/download-artifact@v7
|
||
with:
|
||
name: qemu-native-probes
|
||
path: .artifacts/qemu-native-probes
|
||
|
||
- name: Mark QEMU native probes executable
|
||
run: chmod +x "$VPNHIDE_BIND_BIN"
|
||
|
||
- name: Build module + QEMU runtime test
|
||
# Build the module against the BAKED kernel tree (VPNHIDE_QEMU_KSRC) so
|
||
# CFI/vermagic/struct layouts match the kernel run.sh boots — building
|
||
# against the GKI kdir mismatches and panics on some KMIs (e.g. 6.12).
|
||
# run.sh boots the baked kernel (VPNHIDE_QEMU_IMAGE/ROOTFS from ENV).
|
||
run: |
|
||
CLANG_BIN="$(ls -d /opt/ddk/clang/*/bin | head -1)"
|
||
make -C kmod KERNEL_SRC="$VPNHIDE_QEMU_KSRC" CLANG_DIR="$CLANG_BIN"
|
||
VPNHIDE_QEMU_KO="$PWD/kmod/vpnhide_kmod.ko" kmod/test/run.sh "$KMI"
|
||
|
||
# KPM backend on the SAME baked GKI kernels as kmod-qemu, but patched with
|
||
# KernelPatch instead of insmod (run-kpm.sh). Reuses the ddk-qemu image: the
|
||
# baked Image/rootfs + KernelPatch tool/runtime (VPNHIDE_KP_BIN) make this
|
||
# hermetic. Legacy pre-GKI kernels (4.14/4.19/5.4) are a separate job/image.
|
||
kpm-qemu:
|
||
needs: [setup, qemu-native-probes]
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
kmi:
|
||
- android12-5.10
|
||
- android13-5.10
|
||
- android13-5.15
|
||
- android14-5.15
|
||
- android14-6.1
|
||
- android15-6.6
|
||
- android16-6.12
|
||
container:
|
||
image: ${{ needs.setup.outputs.ddkqemu }}:${{ matrix.kmi }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
env:
|
||
KMI: ${{ matrix.kmi }}
|
||
VPNHIDE_BIND_BIN: ${{ github.workspace }}/.artifacts/qemu-native-probes/bind-probe
|
||
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
submodules: recursive # KernelPatch headers needed by `make kpm`
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Download QEMU native probes
|
||
uses: actions/download-artifact@v7
|
||
with:
|
||
name: qemu-native-probes
|
||
path: .artifacts/qemu-native-probes
|
||
|
||
- name: Mark QEMU native probes executable
|
||
run: chmod +x "$VPNHIDE_BIND_BIN"
|
||
|
||
- name: Build .kpm + KPM QEMU runtime test
|
||
# `make kpm` builds the relocatable .kpm against the KernelPatch
|
||
# submodule (no kernel tree needed). run-kpm.sh patches the baked Image
|
||
# with KernelPatch (VPNHIDE_KP_BIN), embeds the .kpm, and boots the A/B.
|
||
# VPNHIDE_GAI_REQUIRED: the bionic getifaddrs probe is baked into the
|
||
# image, so a skip means it's missing — fail rather than drop coverage.
|
||
env:
|
||
VPNHIDE_GAI_REQUIRED: "1"
|
||
VPNHIDE_BIND_REQUIRED: "1"
|
||
run: |
|
||
CLANG_BIN="$(ls -d /opt/ddk/clang/*/bin | head -1)"
|
||
make -C kmod kpm CLANG_DIR="$CLANG_BIN"
|
||
kmod/test/run-kpm.sh "$KMI"
|
||
|
||
# Legacy pre-GKI kernels (4.14/4.19/5.4) — the KPM's whole reason to exist
|
||
# (the .ko can't serve them). One image holds all three from-source Images;
|
||
# `-cpu cortex-a57` (those kernels fault on `-cpu max`) with software PAN
|
||
# compiled in, so the user/kernel pointer-domain bug class is caught here on
|
||
# the exact cores where it lives.
|
||
kpm-qemu-legacy:
|
||
needs: [setup, qemu-native-probes]
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
ver: ["4.14", "4.19", "5.4"]
|
||
container:
|
||
image: ${{ needs.setup.outputs.kpmlegacy }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
env:
|
||
VPNHIDE_BIND_BIN: ${{ github.workspace }}/.artifacts/qemu-native-probes/bind-probe
|
||
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
submodules: recursive # KernelPatch headers needed by `make kpm`
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Download QEMU native probes
|
||
uses: actions/download-artifact@v7
|
||
with:
|
||
name: qemu-native-probes
|
||
path: .artifacts/qemu-native-probes
|
||
|
||
- name: Mark QEMU native probes executable
|
||
run: chmod +x "$VPNHIDE_BIND_BIN"
|
||
|
||
- name: Build .kpm + legacy KPM QEMU test
|
||
# plain clang from the legacy image; the baked Image + rootfs + KP tools
|
||
# come from ENV (VPNHIDE_QEMU_ROOTFS / VPNHIDE_KP_BIN). The KMI arg is
|
||
# just a label here — VPNHIDE_QEMU_IMAGE picks the from-source kernel.
|
||
# VPNHIDE_GAI_REQUIRED: the bionic getifaddrs probe is baked into the
|
||
# image, so a skip means it's missing — fail rather than drop coverage.
|
||
env:
|
||
VPNHIDE_GAI_REQUIRED: "1"
|
||
VPNHIDE_BIND_REQUIRED: "1"
|
||
run: |
|
||
make -C kmod kpm
|
||
VPNHIDE_QEMU_IMAGE="/opt/qemu/legacy/${{ matrix.ver }}/Image" \
|
||
VPNHIDE_QEMU_CPU=cortex-a57 \
|
||
kmod/test/run-kpm.sh android12-5.10
|
||
|
||
# The KPM ships as ONE cross-version flashable module (Magisk/KSU format,
|
||
# APatch reads it too), not a raw .kpm: `make kpm` builds the relocatable
|
||
# object, kmod/kpm/build.py stages the boot scripts + stamps module.prop.
|
||
# The kpm-qemu / kpm-qemu-legacy jobs above are the runtime *test* gate; this
|
||
# job is the *publish* gate.
|
||
kpm:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
submodules: recursive # KernelPatch headers needed by `make kpm`
|
||
fetch-depth: 0 # get_build_version's `git describe`
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Build KPM module zip
|
||
env:
|
||
UPDATE_JSON_URL: https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-kpm.json
|
||
run: |
|
||
# clang + lld are needed for the freestanding `-r` link. Bake them
|
||
# into the ci image to drop this fallback (one-job apt, same pattern
|
||
# as the shellcheck step); after the rebuild this no-ops in <1s.
|
||
if ! command -v clang >/dev/null || ! command -v ld.lld >/dev/null; then
|
||
apt-get update -qq && apt-get install -y --no-install-recommends clang lld >/dev/null
|
||
fi
|
||
python3 kmod/kpm/build.py
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: vpnhide-kpm
|
||
path: vpnhide-kpm.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@v7
|
||
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@v6
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
zygisk/target
|
||
key: cargo-${{ runner.os }}-${{ hashFiles('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@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Cache cargo
|
||
uses: actions/cache@v6
|
||
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:
|
||
needs: setup
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: ${{ needs.setup.outputs.image }}
|
||
credentials:
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Mark workspace safe
|
||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||
|
||
- name: Cache cargo
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: |
|
||
/usr/local/cargo/registry
|
||
/usr/local/cargo/git
|
||
target
|
||
key: cargo-${{ runner.os }}-portshide-${{ hashFiles('Cargo.lock') }}
|
||
restore-keys: cargo-${{ runner.os }}-portshide-
|
||
|
||
- 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, kpm, 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@v7
|
||
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@v3
|
||
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 }}
|