Build Module #603
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Module | |
| permissions: | |
| contents: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '*/30 * * * *' | |
| jobs: | |
| check-upstream: | |
| name: Check upstream for changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_build: ${{ steps.check.outputs.should_build }} | |
| steps: | |
| - name: Get upstream latest commit SHA | |
| id: upstream | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| SHA=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \ | |
| "https://api.github.com/repos/Flowseal/zapret-discord-youtube/commits/main" \ | |
| | jq -r '.sha') | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| echo "Upstream SHA: $SHA" | |
| - name: Restore cached upstream SHA | |
| if: github.event_name == 'schedule' | |
| id: cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: upstream-sha.txt | |
| key: upstream-sha-${{ steps.upstream.outputs.sha }} | |
| restore-keys: upstream-sha- | |
| - name: Decide whether to build | |
| id: check | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "workflow_dispatch: always build" | |
| elif [ "${{ steps.cache.outputs.cache-hit }}" = "true" ]; then | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| echo "Upstream unchanged, skipping build" | |
| else | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "Upstream changed, triggering build" | |
| fi | |
| - name: Write SHA file for cache | |
| if: steps.check.outputs.should_build == 'true' | |
| run: echo "${{ steps.upstream.outputs.sha }}" > upstream-sha.txt | |
| - name: Save upstream SHA to cache | |
| if: steps.check.outputs.should_build == 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: upstream-sha.txt | |
| key: upstream-sha-${{ steps.upstream.outputs.sha }} | |
| build-zapret: | |
| name: zapret for Android ${{ matrix.abi }} | |
| runs-on: ubuntu-latest | |
| needs: [check-upstream] | |
| if: needs.check-upstream.outputs.should_build == 'true' | |
| strategy: | |
| matrix: | |
| include: | |
| - abi: armeabi-v7a | |
| target: armv7a-linux-androideabi | |
| output: nfqws-arm | |
| - abi: arm64-v8a | |
| target: aarch64-linux-android | |
| output: nfqws-aarch64 | |
| - abi: x86 | |
| target: i686-linux-android | |
| output: nfqws-x86 | |
| - abi: x86_64 | |
| target: x86_64-linux-android | |
| output: nfqws-x86_x64 | |
| steps: | |
| - name: Checkout zapret | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: bol-van/zapret | |
| path: zapret | |
| - name: Build nfqws | |
| env: | |
| ABI: ${{ matrix.abi }} | |
| TARGET: ${{ matrix.target }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| DEPS_DIR=$GITHUB_WORKSPACE/deps | |
| export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64 | |
| export API=21 | |
| export CC="$TOOLCHAIN/bin/clang --target=$TARGET$API" | |
| export AR=$TOOLCHAIN/bin/llvm-ar | |
| export AS=$CC | |
| export LD=$TOOLCHAIN/bin/ld | |
| export RANLIB=$TOOLCHAIN/bin/llvm-ranlib | |
| export STRIP=$TOOLCHAIN/bin/llvm-strip | |
| export PKG_CONFIG_PATH=$DEPS_DIR/lib/pkgconfig | |
| curl -sSL https://www.netfilter.org/pub/libnfnetlink/libnfnetlink-1.0.2.tar.bz2 | tar -xj | |
| curl -sSL https://www.netfilter.org/pub/libmnl/libmnl-1.0.5.tar.bz2 | tar -xj | |
| curl -sSL https://www.netfilter.org/pub/libnetfilter_queue/libnetfilter_queue-1.0.5.tar.bz2 | tar -xj | |
| patch -p1 -d libnetfilter_queue-1.0.5 -i "$GITHUB_WORKSPACE/zapret/.github/workflows/libnetfilter_queue-android.patch" | |
| for i in libmnl libnfnetlink libnetfilter_queue; do | |
| ( | |
| cd $i-* | |
| CFLAGS="-Os -flto=auto -Wno-implicit-function-declaration" ./configure --prefix= --host=$TARGET --enable-static --disable-shared --disable-dependency-tracking | |
| make install -j$(nproc) DESTDIR=$DEPS_DIR | |
| ) | |
| sed -i "s|^prefix=.*|prefix=$DEPS_DIR|g" $DEPS_DIR/lib/pkgconfig/$i.pc | |
| done | |
| CFLAGS="-DZAPRET_GH_VER=${{ github.ref_name }} -DZAPRET_GH_HASH=${{ github.sha }} -I$DEPS_DIR/include" LDFLAGS="-L$DEPS_DIR/lib" make -C zapret android -j$(nproc) | |
| ELFCLEANER_TAG="$(gh api repos/termux/termux-elf-cleaner/releases/latest --jq '.tag_name')" | |
| curl -fsSL -o elf-cleaner "https://github.com/termux/termux-elf-cleaner/releases/download/${ELFCLEANER_TAG}/termux-elf-cleaner" | |
| chmod +x elf-cleaner | |
| ./elf-cleaner --api-level 21 zapret/binaries/my/* | |
| mv zapret/binaries/my/nfqws "$PWD/${{ matrix.output }}" | |
| - name: Upload nfqws artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.output }} | |
| path: ${{ matrix.output }} | |
| if-no-files-found: error | |
| build-dnscrypt: | |
| name: dnscrypt-proxy for Android | |
| runs-on: ubuntu-latest | |
| needs: [check-upstream] | |
| if: needs.check-upstream.outputs.should_build == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Checkout dnscrypt-proxy | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: DNSCrypt/dnscrypt-proxy | |
| path: dnscrypt-proxy | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1" | |
| check-latest: true | |
| cache-dependency-path: "dnscrypt-proxy/**/go.sum" | |
| - name: Build dnscrypt-proxy binaries | |
| run: | | |
| SRC_DIR="${GITHUB_WORKSPACE}/dnscrypt-proxy" | |
| if [ -d "${SRC_DIR}/dnscrypt-proxy" ]; then | |
| SRC_DIR="${SRC_DIR}/dnscrypt-proxy" | |
| fi | |
| OUTPUT_DIR="${GITHUB_WORKSPACE}/dnscrypt-proxy/binaries" | |
| mkdir -p "${OUTPUT_DIR}" | |
| cd "${SRC_DIR}" | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/modified-ci-build.sh" | |
| mv dnscrypt-proxy-* "${OUTPUT_DIR}/" | |
| - name: Upload dnscrypt-proxy artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dnscrypt-proxy | |
| path: dnscrypt-proxy/binaries/* | |
| if-no-files-found: error | |
| build-curl: | |
| name: curl from Termux packages | |
| runs-on: ubuntu-latest | |
| needs: [check-upstream] | |
| if: needs.check-upstream.outputs.should_build == 'true' | |
| steps: | |
| - name: Download and extract curl binaries from Termux .deb packages | |
| run: | | |
| set -e | |
| BASE_URL="https://packages.termux.dev/apt/termux-main/pool/main/c/curl" | |
| mkdir -p curl-debs curl-work binaries | |
| curl -fsSL "$BASE_URL/" -o curl-index.html | |
| mapfile -t debs < <(grep -oE 'curl_[^"<>[:space:]]+_(aarch64|arm|i686|x86_64)\.deb' curl-index.html | sort -u) | |
| [ "${#debs[@]}" -eq 4 ] || { | |
| echo "! Expected 4 curl .deb files, got ${#debs[@]}" | |
| printf '%s\n' "${debs[@]}" | |
| exit 1 | |
| } | |
| for deb in "${debs[@]}"; do | |
| echo "Downloading $deb" | |
| curl -fsSL "$BASE_URL/$deb" -o "curl-debs/$deb" | |
| pkg_dir="curl-work/${deb%.deb}" | |
| root_dir="$pkg_dir/root" | |
| mkdir -p "$pkg_dir" "$root_dir" | |
| ( | |
| cd "$pkg_dir" | |
| ar x "../../curl-debs/$deb" | |
| ) | |
| data_archive="$(find "$pkg_dir" -maxdepth 1 -type f \( -name 'data.tar.xz' -o -name 'data.tar.gz' -o -name 'data.tar.zst' -o -name 'data.tar' \) | head -n1)" | |
| [ -n "$data_archive" ] || { echo "! data archive not found in $deb"; exit 1; } | |
| case "$data_archive" in | |
| *.tar.xz) tar -xJf "$data_archive" -C "$root_dir" ;; | |
| *.tar.gz) tar -xzf "$data_archive" -C "$root_dir" ;; | |
| *.tar.zst) tar --zstd -xf "$data_archive" -C "$root_dir" ;; | |
| *.tar) tar -xf "$data_archive" -C "$root_dir" ;; | |
| *) echo "! Unsupported archive format: $data_archive"; exit 1 ;; | |
| esac | |
| bin_path="$(find "$root_dir" -type f -path '*/data/data/com.termux/files/usr/bin/curl' | head -n1)" | |
| [ -n "$bin_path" ] || { echo "! curl binary not found inside $deb"; exit 1; } | |
| case "$deb" in | |
| *_aarch64.deb) out_name="curl-aarch64" ;; | |
| *_arm.deb) out_name="curl-arm" ;; | |
| *_i686.deb) out_name="curl-x86" ;; | |
| *_x86_64.deb) out_name="curl-x86_64" ;; | |
| *) echo "! Unknown ABI for $deb"; exit 1 ;; | |
| esac | |
| install -m 0755 "$bin_path" "binaries/$out_name" | |
| file "binaries/$out_name" || true | |
| done | |
| - name: Upload curl artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: curl-binaries | |
| path: binaries/* | |
| if-no-files-found: error | |
| validate-and-package: | |
| name: Validate and package module | |
| runs-on: ubuntu-latest | |
| needs: [check-upstream, build-zapret, build-dnscrypt, build-curl] | |
| if: needs.check-upstream.outputs.should_build == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.ref_name }} | |
| - name: Determine version info | |
| id: version | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| # Generate timestamp in Moscow time (UTC+3) | |
| BUILD_TIME=$(TZ='Europe/Moscow' date '+%d-%m-%y %H:%M:%S') | |
| RELEASE_COUNT="$(gh api "repos/${REPO}/releases" --paginate --jq 'length' | awk '{s+=$1} END {print s+0}')" | |
| VERSION_CODE=$((RELEASE_COUNT + 1)) | |
| TAG="${VERSION_CODE}" | |
| VERSION="v${TAG}" | |
| RELEASE_NAME="v${VERSION_CODE}" | |
| # Fetch repo description from GitHub API | |
| DESCRIPTION="$(gh api "repos/${REPO}" --jq '.description // "Zapret module"')" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "version_code=${VERSION_CODE}" >> "$GITHUB_OUTPUT" | |
| echo "description=${DESCRIPTION}" >> "$GITHUB_OUTPUT" | |
| echo "release_name=${RELEASE_NAME}" >> "$GITHUB_OUTPUT" | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "Version : ${VERSION}" | |
| echo "VersionCode : ${VERSION_CODE}" | |
| echo "ReleaseCount : ${RELEASE_COUNT}" | |
| echo "Release Name : ${RELEASE_NAME}" | |
| echo "Tag : ${TAG}" | |
| echo "Description : ${DESCRIPTION}" | |
| - name: Patch module.prop | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| VERSION_CODE="${{ steps.version.outputs.version_code }}" | |
| DESCRIPTION="${{ steps.version.outputs.description }}" | |
| sed -i "s|^version=.*|version=${VERSION}|" module/module.prop | |
| sed -i "s|^versionCode=.*|versionCode=${VERSION_CODE}|" module/module.prop | |
| sed -i "s|^description=.*|description=${DESCRIPTION}|" module/module.prop | |
| echo "=== module.prop ===" | |
| cat module/module.prop | |
| - name: Patch update.json | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| VERSION_CODE="${{ steps.version.outputs.version_code }}" | |
| TAG="${{ steps.version.outputs.tag }}" | |
| REPO="${{ github.repository }}" | |
| cat > update.json <<EOF | |
| { | |
| "version": "${VERSION}", | |
| "versionCode": "${VERSION_CODE}", | |
| "zipUrl": "https://github.com/${REPO}/releases/download/${TAG}/zapret-pocket.zip", | |
| "changelog": "https://raw.githubusercontent.com/${REPO}/main/CHANGELOG.md" | |
| } | |
| EOF | |
| echo "=== update.json ===" | |
| cat update.json | |
| - name: Generate CHANGELOG.md | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| python3 - <<'PYEOF' | |
| import subprocess | |
| import os | |
| import textwrap | |
| repo = os.environ["REPO"] | |
| tags_raw = subprocess.check_output( | |
| ["git", "tag", "--list", "--sort=-creatordate"], | |
| text=True, | |
| ).splitlines() | |
| prev_tag = next((tag for tag in tags_raw if tag.isdigit()), "") | |
| log_cmd = ["git", "log", "--format=%h %s"] | |
| if prev_tag: | |
| log_cmd = ["git", "log", f"{prev_tag}..HEAD", "--format=%h %s"] | |
| commits_raw = subprocess.check_output(log_cmd, text=True).splitlines() | |
| if not commits_raw: | |
| commits_raw = [subprocess.check_output(["git", "log", "-1", "--format=%h %s"], text=True).strip()] | |
| lines = [] | |
| for entry in commits_raw: | |
| parts = entry.split(" ", 1) | |
| short_sha = parts[0] | |
| msg = parts[1] if len(parts) > 1 else short_sha | |
| lines.append(f"- {msg} {short_sha}") | |
| body = "\n".join(lines) | |
| # Static links header | |
| header = textwrap.dedent("""\ | |
| [Telegram Channel](https://t.me/sevcator/921) | |
| [Repository](https://github.com/sevcator/zapret-pocket/) | |
| [Report Issues](https://github.com/sevcator/zapret-pocket/issues) | |
| [Donate](https://t.me/sevcator/909) | |
| [Author](https://github.com/sevcator/) | |
| """) | |
| content = header + "\n" + body + "\n" | |
| with open("CHANGELOG.md", "w", encoding="utf-8") as f: | |
| f.write(content) | |
| print("=== CHANGELOG.md ===") | |
| print(content) | |
| PYEOF | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Download upstream archive | |
| run: | | |
| curl -L -o flowseal-upstream.zip https://github.com/Flowseal/zapret-discord-youtube/archive/refs/heads/main.zip | |
| - name: Sync module assets from upstream zip | |
| run: | | |
| python .github/scripts/generate-assets.py | |
| - name: Download default ipset-service.txt into module cache | |
| run: | | |
| mkdir -p module/.service | |
| curl -fsSL \ | |
| -o module/.service/ipset-service.txt \ | |
| https://raw.githubusercontent.com/sevcator/zapret-lists/refs/heads/main/ipset-service.txt | |
| - name: Validate shell syntax | |
| run: | | |
| find module -type f \( -name "*.sh" -o -path "module/system/bin/zapret" \) -print0 | xargs -0 -n1 sh -n | |
| - name: Download nfqws armeabi-v7a | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nfqws-arm | |
| path: artifacts/nfqws-arm | |
| - name: Download nfqws arm64-v8a | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nfqws-aarch64 | |
| path: artifacts/nfqws-aarch64 | |
| - name: Download nfqws x86 | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nfqws-x86 | |
| path: artifacts/nfqws-x86 | |
| - name: Download nfqws x86_64 | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nfqws-x86_x64 | |
| path: artifacts/nfqws-x86_x64 | |
| - name: Download dnscrypt-proxy | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dnscrypt-proxy | |
| path: artifacts/dnscrypt-proxy | |
| - name: Download curl binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: curl-binaries | |
| path: artifacts/curl | |
| - name: Download latest VpnHotspot APK | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| python - <<'PY' | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from pathlib import Path | |
| from urllib.request import Request, urlopen | |
| api_url = "https://api.github.com/repos/Mygod/VPNHotspot/releases" | |
| headers = { | |
| "Accept": "application/vnd.github+json", | |
| "Authorization": f"Bearer {os.environ['GH_TOKEN']}", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| } | |
| request = Request(api_url, headers=headers) | |
| with urlopen(request) as response: | |
| releases = json.load(response) | |
| asset = None | |
| for release in releases: | |
| if release.get("draft") or release.get("prerelease"): | |
| continue | |
| for candidate in release.get("assets", []): | |
| name = candidate.get("name", "") | |
| if name.startswith("vpnhotspot-v") and name.endswith(".apk"): | |
| asset = candidate | |
| break | |
| if asset is not None: | |
| break | |
| if asset is None: | |
| raise SystemExit("Stable VpnHotspot APK asset was not found") | |
| output_dir = Path("artifacts") / "vpnhotspot" | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| output_path = output_dir / "VpnHotspot.apk" | |
| download_request = Request(asset["browser_download_url"], headers=headers) | |
| with urlopen(download_request) as response: | |
| output_path.write_bytes(response.read()) | |
| print(f"Downloaded {asset['name']} from {asset['browser_download_url']}") | |
| PY | |
| - name: Add binaries to module layout | |
| run: | | |
| mkdir -p module/zapret module/dnscrypt module/system/app | |
| mv artifacts/nfqws-arm/nfqws-arm module/zapret/nfqws-arm | |
| mv artifacts/nfqws-aarch64/nfqws-aarch64 module/zapret/nfqws-aarch64 | |
| mv artifacts/nfqws-x86/nfqws-x86 module/zapret/nfqws-x86 | |
| mv artifacts/nfqws-x86_x64/nfqws-x86_x64 module/zapret/nfqws-x86_x64 | |
| mv artifacts/dnscrypt-proxy/* module/dnscrypt/ | |
| mv artifacts/curl/* module/ | |
| mv artifacts/vpnhotspot/VpnHotspot.apk module/system/app/VpnHotspot.apk | |
| chmod +x module/zapret/*.sh module/zapret/nfqws-* module/dnscrypt/dnscrypt-proxy-* module/dnscrypt/*.sh module/curl-* 2>/dev/null || true | |
| - name: Package module zip | |
| run: | | |
| cd module | |
| find . -type d -empty -delete | |
| zip -r ../zapret-pocket.zip . | |
| - name: Upload module zip artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: zapret-pocket | |
| path: zapret-pocket.zip | |
| if-no-files-found: error | |
| - name: Commit updated files back to repo | |
| run: | | |
| git config user.name "sevcator" | |
| git config user.email "68725282+sevcator@users.noreply.github.com" | |
| git add module/module.prop update.json CHANGELOG.md | |
| if git diff --cached --quiet; then | |
| echo "Nothing to commit." | |
| else | |
| git commit -m "chore: update version, changelog, update.json [skip ci]" | |
| git push origin HEAD:${{ github.ref_name }} || git push origin HEAD:main | |
| fi | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| RELEASE_NAME: ${{ steps.version.outputs.release_name }} | |
| run: | | |
| TAG="${{ steps.version.outputs.tag }}" | |
| # Generate release notes: simple format | |
| python3 - <<'PYEOF' | |
| import subprocess | |
| import os | |
| tags_raw = subprocess.check_output( | |
| ["git", "tag", "--list", "--sort=-creatordate"], | |
| text=True, | |
| ).splitlines() | |
| prev_tag = next((tag for tag in tags_raw if tag.isdigit()), "") | |
| log_cmd = ["git", "log", "--format=%h %s"] | |
| if prev_tag: | |
| log_cmd = ["git", "log", f"{prev_tag}..HEAD", "--format=%h %s"] | |
| commits_raw = subprocess.check_output(log_cmd, text=True).splitlines() | |
| if not commits_raw: | |
| commits_raw = [subprocess.check_output(["git", "log", "-1", "--format=%h %s"], text=True).strip()] | |
| lines = [] | |
| for entry in commits_raw: | |
| parts = entry.split(" ", 1) | |
| short_sha = parts[0] | |
| msg = parts[1] if len(parts) > 1 else short_sha | |
| lines.append(f"- {msg} {short_sha}") | |
| text = "\n".join(lines) + "\n" | |
| with open("/tmp/release_notes.md", "w", encoding="utf-8") as out: | |
| out.write(text) | |
| print("=== Release Notes ===") | |
| print(text) | |
| PYEOF | |
| # Create tag | |
| git tag -f "${TAG}" | |
| git push origin -f "refs/tags/${TAG}" | |
| # Delete existing release with this tag if present | |
| gh release delete "${TAG}" --repo "${REPO}" --yes 2>/dev/null || true | |
| # Create release | |
| gh release create "${TAG}" --repo "${REPO}" --title "${RELEASE_NAME}" --notes-file /tmp/release_notes.md zapret-pocket.zip |