RKNnoVPN/.github/workflows/build.yml
2026-06-07 21:21:47 +03:00

686 lines
24 KiB
YAML

name: Build & Release
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
permissions:
contents: write
env:
GO_VERSION: "1.22"
SINGBOX_GO_VERSION: "1.24.x"
SINGBOX_VERSION: "1.13.0"
SINGBOX_TAGS: "with_quic,with_wireguard,with_utls,with_clash_api,badlinkname,tfogo_checklinkname0"
XRAY_GO_VERSION: "1.24.x"
XRAY_VERSION: "stable"
ANDROID_API: "23"
ANDROID_NDK_VERSION: "27.2.12479018"
jobs:
# ============================================================
# Job -1: Privacy/static policy guardrails
# ============================================================
privacy-lint:
name: Privacy Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check APK privacy surface
run: bash .github/scripts/check_apk_privacy.sh
- name: Check reset.lock script contract
run: bash .github/scripts/check_reset_lock_contract.sh
- name: Check runtime snapshot contract
run: python3 .github/scripts/check_runtime_snapshot_contract.py
- name: Check release manifest metadata
run: bash .github/scripts/check_release_manifest.sh
# ============================================================
# Job -0.5: Local-first runtime architecture guardrail
# ============================================================
local-first-runtime-guardrail:
name: Local-first Runtime Guardrail
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check local-first runtime boundaries
run: bash .github/scripts/check_local_first_runtime.sh
# ============================================================
# Job 0: Go unit/policy tests
# ============================================================
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Test runtime packages
working-directory: runtime
run: go test ./...
# ============================================================
# Job 0.5: Android debug build + unit/instrumentation tests
# ============================================================
android-test:
name: Android Guardrails & Tests
runs-on: ubuntu-latest
needs: [privacy-lint, local-first-runtime-guardrail]
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Check local-first runtime boundaries
run: bash .github/scripts/check_local_first_runtime.sh
- name: Assemble debug APK
working-directory: app
run: ./gradlew :app:assembleDebug --no-daemon --stacktrace
- name: Run Android unit tests
working-directory: app
run: ./gradlew :app:testDebugUnitTest --no-daemon --stacktrace
- name: Detect Android instrumentation tests
id: android_instrumentation_tests
working-directory: app
run: |
if find app/src/androidTest -type f \( -name '*.kt' -o -name '*.java' \) 2>/dev/null | grep -q .; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
fi
- name: Run Android instrumentation tests
if: steps.android_instrumentation_tests.outputs.present == 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
working-directory: app
script: ./gradlew connectedDebugAndroidTest --no-daemon --stacktrace
# ============================================================
# Job 1: Resolve sing-box release tag
# ============================================================
resolve-singbox:
name: Resolve sing-box release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Resolve sing-box release
id: resolve
env:
GH_TOKEN: ${{ github.token }}
run: |
case "${SINGBOX_VERSION}" in
stable|"")
VERSION="$(gh release view --repo SagerNet/sing-box --json tagName --jq .tagName)"
;;
latest|alpha|prerelease)
VERSION="$(gh release list --repo SagerNet/sing-box --limit 1 --json tagName --jq '.[0].tagName')"
;;
*)
VERSION="v${SINGBOX_VERSION#v}"
;;
esac
if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then
echo "Failed to resolve sing-box release" >&2
exit 1
fi
echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT"
echo "ref=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved sing-box $VERSION"
# ============================================================
# Job 2: Build root runtime CLI for Android ABIs
# ============================================================
build-runtime:
name: Build Runtime CLI (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: [go-test]
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
goarch: arm64
goarm: ""
- arch: armv7
goarch: arm
goarm: "7"
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Build runtime CLI
working-directory: runtime
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${GITHUB_REF_NAME}"
CODE="$(../tools/version.sh code "$VERSION")"
else
VERSION="dev-${GITHUB_RUN_NUMBER}-${GITHUB_SHA::7}"
CODE="${GITHUB_RUN_NUMBER}"
fi
mkdir -p "../out/${{ matrix.arch }}"
CGO_ENABLED=0 GOOS=linux GOARCH=${{ matrix.goarch }} GOARM=${{ matrix.goarm }} go build \
-ldflags="-s -w -X main.Version=${VERSION} -X main.VersionCode=${CODE}" \
-o "../out/${{ matrix.arch }}/rknnovpn-runtime" ./cmd/rknnovpn-runtime
- name: Upload runtime CLI artifact
uses: actions/upload-artifact@v4
with:
name: runtime-cli-${{ matrix.arch }}
path: out/${{ matrix.arch }}/
retention-days: 1
# ============================================================
# Job 3: Build Android sing-box binaries for Android module runtime
# ============================================================
build-singbox:
name: Build sing-box (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: [resolve-singbox]
env:
SINGBOX_RESOLVED_VERSION: ${{ needs.resolve-singbox.outputs.version }}
SINGBOX_REF: ${{ needs.resolve-singbox.outputs.ref }}
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
goarch: arm64
goarm: ""
cc: aarch64-linux-android23-clang
linker: /system/bin/linker64
- arch: armv7
goarch: arm
goarm: "7"
cc: armv7a-linux-androideabi23-clang
linker: /system/bin/linker
steps:
- name: Checkout sing-box
uses: actions/checkout@v4
with:
repository: SagerNet/sing-box
ref: ${{ env.SINGBOX_REF }}
path: sing-box-src
- name: Setup Go for sing-box
uses: actions/setup-go@v5
with:
go-version: ${{ env.SINGBOX_GO_VERSION }}
cache-dependency-path: sing-box-src/go.sum
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Install Android NDK
run: |
SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager"
if [[ ! -x "$SDKMANAGER" ]]; then
SDKMANAGER="$(command -v sdkmanager || true)"
fi
if [[ -z "$SDKMANAGER" ]]; then
echo "sdkmanager not found" >&2
exit 127
fi
yes | "$SDKMANAGER" --licenses >/dev/null
"$SDKMANAGER" --install "ndk;${ANDROID_NDK_VERSION}"
echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}" >> "$GITHUB_ENV"
- name: Build Android sing-box
run: |
mkdir -p "out/${{ matrix.arch }}"
cd sing-box-src
if [[ -f release/LDFLAGS ]]; then
LDFLAGS="$(cat release/LDFLAGS)"
else
LDFLAGS="-X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -checklinkname=0"
fi
export CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{ matrix.cc }}"
CGO_ENABLED=1 GOOS=android GOARCH=${{ matrix.goarch }} GOARM=${{ matrix.goarm }} go build \
-trimpath \
-tags "${SINGBOX_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${SINGBOX_RESOLVED_VERSION}' ${LDFLAGS} -s -w -buildid=" \
-o "../out/${{ matrix.arch }}/sing-box" ./cmd/sing-box
cd ..
chmod 755 "out/${{ matrix.arch }}/sing-box"
if ! readelf -l "out/${{ matrix.arch }}/sing-box" | grep -q "Requesting program interpreter: ${{ matrix.linker }}"; then
echo "ERROR: sing-box is not an Android PIE binary for ${{ matrix.arch }}"
readelf -l "out/${{ matrix.arch }}/sing-box"
exit 1
fi
readelf -h "out/${{ matrix.arch }}/sing-box"
sha256sum "out/${{ matrix.arch }}/sing-box"
- name: Upload sing-box artifact
uses: actions/upload-artifact@v4
with:
name: singbox-${{ matrix.arch }}
path: out/${{ matrix.arch }}/
retention-days: 1
# ============================================================
# Job 4: Build Android Xray-core sidecar for VLESS/XHTTP only
# ============================================================
resolve-xray:
name: Resolve Xray-core release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Resolve Xray-core release
id: resolve
env:
GH_TOKEN: ${{ github.token }}
run: |
case "${XRAY_VERSION}" in
stable|"")
VERSION="$(gh release view --repo XTLS/Xray-core --json tagName --jq .tagName)"
;;
latest|alpha|prerelease)
VERSION="$(gh release list --repo XTLS/Xray-core --limit 1 --json tagName --jq '.[0].tagName')"
;;
v*)
VERSION="${XRAY_VERSION}"
;;
*)
VERSION="v${XRAY_VERSION#v}"
;;
esac
if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then
echo "Failed to resolve Xray-core release" >&2
exit 1
fi
echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT"
echo "ref=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved Xray-core $VERSION"
build-xray:
name: Build Xray-core (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: [resolve-xray]
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
goarch: arm64
goarm: ""
cc: aarch64-linux-android23-clang
linker: /system/bin/linker64
- arch: armv7
goarch: arm
goarm: "7"
cc: armv7a-linux-androideabi23-clang
linker: /system/bin/linker
steps:
- name: Checkout Xray-core
uses: actions/checkout@v4
with:
repository: XTLS/Xray-core
ref: ${{ needs.resolve-xray.outputs.ref }}
path: xray-src
- name: Setup Go for Xray-core
uses: actions/setup-go@v5
with:
go-version: ${{ env.XRAY_GO_VERSION }}
cache-dependency-path: xray-src/go.sum
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Install Android NDK
run: |
SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager"
if [[ ! -x "$SDKMANAGER" ]]; then
SDKMANAGER="$(command -v sdkmanager || true)"
fi
if [[ -z "$SDKMANAGER" ]]; then
echo "sdkmanager not found" >&2
exit 127
fi
yes | "$SDKMANAGER" --licenses >/dev/null
"$SDKMANAGER" --install "ndk;${ANDROID_NDK_VERSION}"
echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}" >> "$GITHUB_ENV"
- name: Build Android Xray-core
run: |
mkdir -p "out/${{ matrix.arch }}"
cd xray-src
export CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{ matrix.cc }}"
CGO_ENABLED=1 GOOS=android GOARCH=${{ matrix.goarch }} GOARM=${{ matrix.goarm }} go build \
-trimpath \
-ldflags "-s -w -buildid=" \
-o "../out/${{ matrix.arch }}/xray" ./main
cd ..
chmod 755 "out/${{ matrix.arch }}/xray"
if ! readelf -l "out/${{ matrix.arch }}/xray" | grep -q "Requesting program interpreter: ${{ matrix.linker }}"; then
echo "ERROR: xray is not an Android PIE binary for ${{ matrix.arch }}"
readelf -l "out/${{ matrix.arch }}/xray"
exit 1
fi
readelf -h "out/${{ matrix.arch }}/xray"
sha256sum "out/${{ matrix.arch }}/xray"
- name: Upload Xray-core artifact
uses: actions/upload-artifact@v4
with:
name: xray-${{ matrix.arch }}
path: out/${{ matrix.arch }}/
retention-days: 1
# ============================================================
# Job 5: Assemble Magisk module ZIP
# ============================================================
build-module:
name: Build Magisk Module
runs-on: ubuntu-latest
needs: [resolve-singbox, build-runtime, build-singbox, build-xray]
outputs:
singbox_version: ${{ needs.resolve-singbox.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Download runtime CLI
uses: actions/download-artifact@v4
with:
name: runtime-cli-arm64
path: module/binaries/arm64/
- name: Download runtime CLI (armv7)
uses: actions/download-artifact@v4
with:
name: runtime-cli-armv7
path: module/binaries/armv7/
- name: Download sing-box binary
uses: actions/download-artifact@v4
with:
name: singbox-arm64
path: module/binaries/arm64/
- name: Download sing-box binary (armv7)
uses: actions/download-artifact@v4
with:
name: singbox-armv7
path: module/binaries/armv7/
- name: Download Xray-core binary
uses: actions/download-artifact@v4
with:
name: xray-arm64
path: module/binaries/arm64/
- name: Download Xray-core binary (armv7)
uses: actions/download-artifact@v4
with:
name: xray-armv7
path: module/binaries/armv7/
- name: Set permissions
run: chmod 755 module/binaries/arm64/* module/binaries/armv7/*
- name: Stamp version in module.prop
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${GITHUB_REF_NAME}"
CODE="$(tools/version.sh code "$VERSION")"
else
VERSION="dev-${GITHUB_RUN_NUMBER}-${GITHUB_SHA::7}"
CODE="${GITHUB_RUN_NUMBER}"
fi
sed -i "s/^version=.*/version=${VERSION}/" module/module.prop
sed -i "s/^versionCode=.*/versionCode=${CODE}/" module/module.prop
cat module/module.prop
- name: Build ZIP
run: |
cd module
zip -r ../rknnovpn-module.zip . \
-x "*.git*" "*.DS_Store"
cd ..
for dir in arm64 armv7; do
unzip -l rknnovpn-module.zip "binaries/${dir}/rknnovpn-runtime" "binaries/${dir}/sing-box" "binaries/${dir}/xray" >/dev/null
done
sha256sum rknnovpn-module.zip
- name: Upload module ZIP
uses: actions/upload-artifact@v4
with:
name: rknnovpn-module
path: rknnovpn-module.zip
retention-days: 5
# ============================================================
# Job 5: Build Android APK
# ============================================================
build-apk:
name: Build APK
runs-on: ubuntu-latest
needs: [privacy-lint, android-test]
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Stamp version
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${GITHUB_REF_NAME}"
VER_NAME="$VERSION"
VER_CODE="$(tools/version.sh code "$VERSION")"
else
VER_NAME="dev-${GITHUB_RUN_NUMBER}-${GITHUB_SHA::7}"
VER_CODE="${GITHUB_RUN_NUMBER}"
fi
echo "RKNNOVPN_VERSION=${VER_NAME}" >> "$GITHUB_ENV"
echo "RKNNOVPN_VERSION_CODE=${VER_CODE}" >> "$GITHUB_ENV"
- name: Build and sign APK
working-directory: app
env:
RKNNOVPN_RELEASE_KEYSTORE_BASE64: ${{ secrets.RKNNOVPN_RELEASE_KEYSTORE_BASE64 }}
RKNNOVPN_RELEASE_STORE_PASSWORD: ${{ secrets.RKNNOVPN_RELEASE_STORE_PASSWORD }}
RKNNOVPN_RELEASE_KEY_ALIAS: ${{ secrets.RKNNOVPN_RELEASE_KEY_ALIAS }}
RKNNOVPN_RELEASE_KEY_PASSWORD: ${{ secrets.RKNNOVPN_RELEASE_KEY_PASSWORD }}
run: |
STORE_FILE="${RUNNER_TEMP}/rknnovpn-release.jks"
if [ -n "${RKNNOVPN_RELEASE_KEYSTORE_BASE64:-}" ]; then
printf '%s' "${RKNNOVPN_RELEASE_KEYSTORE_BASE64}" | base64 -d > "${STORE_FILE}"
elif [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: stable tag builds require RKNNOVPN_RELEASE_KEYSTORE_BASE64 signing secret" >&2
exit 1
else
RKNNOVPN_RELEASE_STORE_PASSWORD="dev-release-store-${GITHUB_RUN_ID}"
RKNNOVPN_RELEASE_KEY_PASSWORD="${RKNNOVPN_RELEASE_STORE_PASSWORD}"
RKNNOVPN_RELEASE_KEY_ALIAS="rknnovpn-dev"
keytool -genkeypair \
-keystore "${STORE_FILE}" \
-storepass "${RKNNOVPN_RELEASE_STORE_PASSWORD}" \
-keypass "${RKNNOVPN_RELEASE_KEY_PASSWORD}" \
-alias "${RKNNOVPN_RELEASE_KEY_ALIAS}" \
-keyalg RSA \
-keysize 4096 \
-validity 30 \
-dname "CN=RKNnoVPN Dev Release,O=RKNnoVPN,C=RU"
fi
export RKNNOVPN_RELEASE_STORE_FILE="${STORE_FILE}"
export RKNNOVPN_RELEASE_STORE_PASSWORD
export RKNNOVPN_RELEASE_KEY_ALIAS
export RKNNOVPN_RELEASE_KEY_PASSWORD
./gradlew assembleRelease --no-daemon --stacktrace
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
if [ -z "$APK" ]; then
echo "ERROR: No APK found!"
find . -name "*.apk" -ls
exit 1
fi
echo "APK found: $APK"
cp "$APK" ../rknnovpn-panel.apk
bash ../.github/scripts/check_release_apk_privacy.sh ../rknnovpn-panel.apk
sha256sum ../rknnovpn-panel.apk
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: rknnovpn-apk
path: rknnovpn-panel.apk
retention-days: 5
# ============================================================
# Job 6: Create GitHub Release
# ============================================================
release:
name: Create Release
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
needs: [build-module, build-apk]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve release version
id: version
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${GITHUB_REF_NAME}"
STABLE=true
elif [[ "${GITHUB_REF_NAME}" == v* ]]; then
echo "ERROR: release tags must be strict vMAJOR.MINOR.PATCH, got ${GITHUB_REF_NAME}" >&2
exit 1
else
VERSION="dev-${GITHUB_RUN_NUMBER}-${GITHUB_SHA::7}"
STABLE=false
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stable=${STABLE}" >> "$GITHUB_OUTPUT"
- name: Download Magisk module
uses: actions/download-artifact@v4
with:
name: rknnovpn-module
path: release/
- name: Download APK
uses: actions/download-artifact@v4
with:
name: rknnovpn-apk
path: release/
- name: Rename with version
run: |
VERSION="${{ steps.version.outputs.version }}"
mv release/rknnovpn-module.zip "release/rknnovpn-${VERSION}-module.zip"
mv release/rknnovpn-panel.apk "release/rknnovpn-${VERSION}-panel.apk"
# Generate checksums
cd release && sha256sum * > SHA256SUMS.txt && cd ..
- name: Update update.json for Magisk manager
if: steps.version.outputs.stable == 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
VERSION_CODE="$(tools/version.sh code "$VERSION")"
cat > update.json <<ENDJSON
{
"version": "${VERSION}",
"versionCode": ${VERSION_CODE},
"zipUrl": "https://github.com/youtubediscord/RKNnoVPN/releases/download/${VERSION}/rknnovpn-${VERSION}-module.zip",
"changelog": "https://github.com/youtubediscord/RKNnoVPN/releases/tag/${VERSION}"
}
ENDJSON
# Commit and push the updated update.json to main
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add update.json
git diff --cached --quiet || git commit -m "chore: update update.json for ${VERSION} [skip ci]"
git fetch origin main
git merge-base --is-ancestor origin/main HEAD
git push origin HEAD:main
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.version }}
target_commitish: ${{ github.sha }}
name: "RKNnoVPN ${{ steps.version.outputs.version }}"
draft: false
prerelease: ${{ steps.version.outputs.stable != 'true' }}
generate_release_notes: true
body: |
## RKNnoVPN ${{ steps.version.outputs.version }}
### Downloads
| File | Description |
|------|-------------|
| `rknnovpn-${{ steps.version.outputs.version }}-module.zip` | Magisk/KernelSU/APatch module - flash via manager |
| `rknnovpn-${{ steps.version.outputs.version }}-panel.apk` | Controller APK - install on same device |
### Quick Start
1. Flash the module ZIP via Magisk Manager / KSU Manager / APatch
2. Reboot
3. Install the APK
4. Open RKNnoVPN Panel and add a server (paste VLESS/Trojan/SS link)
5. Select apps to proxy in the Apps tab
6. Tap Connect
### What's inside
- **No VPN, no TUN, no icon** - uses tproxy+iptables at kernel level
- **Whitelist mode** - only selected apps go through proxy
- **Full IPv4+IPv6** - mirrored iptables/ip6tables rules
- **sing-box v${{ needs.build-module.outputs.singbox_version }}** - VLESS, Trojan, VMess, SS, SOCKS, Hysteria2, TUIC
- **Xray-core sidecar** - used only for VLESS/XHTTP profiles
- **Explicit cleanup** - reset/cleanup scripts for root networking state
files: |
release/rknnovpn-${{ steps.version.outputs.version }}-module.zip
release/rknnovpn-${{ steps.version.outputs.version }}-panel.apk
release/SHA256SUMS.txt