Skip to content

Build Module

Build Module #603

Workflow file for this run

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