vpnhide/.github/workflows/ci.yml
2026-07-13 22:01:17 +03:00

720 lines
26 KiB
YAML
Raw Permalink 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
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.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:
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 }}