magisk-zapret2/.forgejo/workflows/build.yml
loop-uh 6a9db45891
All checks were successful
Background release validation / Package Magisk Module (push) Successful in 15s
Background release validation / Prepare Version (push) Successful in 3s
Background release validation / Build Android APK (push) Successful in 4m57s
Background release validation / Fetch latest upstream zapret2 release (push) Successful in 5s
Magisk source guards / guards (push) Successful in 3s
Background release validation / Shell integration tests (push) Successful in 1m37s
Получать проверенный zapret2 из Forgejo
2026-08-07 06:56:13 +03:00

484 lines
21 KiB
YAML

name: Background release validation
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
shell-tests:
name: Shell integration tests
runs-on: android
needs: fetch-upstream
steps:
- name: Checkout
uses: https://data.forgejo.org/actions/checkout@v6
- name: Validate owned source hygiene
run: |
set -euo pipefail
EMPTY_TREE=4b825dc642cb6eb9a060e54bf8d69288fbee4904
git diff --check "$EMPTY_TREE" "$FORGEJO_SHA" -- .
- name: Reject committed secrets and release debug flags
run: |
set -euo pipefail
bash tests/release/source-policy.sh
- name: Download verified upstream payload
uses: https://data.forgejo.org/forgejo/download-artifact@v4
with:
name: upstream-zapret2-payload
path: upstream-payload
- name: Hydrate upstream files required by package fixtures
run: |
set -euo pipefail
mkdir -p zapret2/bin/arm64-v8a zapret2/bin/armeabi-v7a
install -m 0755 upstream-payload/bin/arm64-v8a/nfqws2 zapret2/bin/arm64-v8a/nfqws2
install -m 0755 upstream-payload/bin/armeabi-v7a/nfqws2 zapret2/bin/armeabi-v7a/nfqws2
while IFS= read -r upstream_lua || [ -n "$upstream_lua" ]; do
[ -z "$upstream_lua" ] && continue
install -m 0644 "upstream-payload/lua/$upstream_lua" "zapret2/lua/$upstream_lua"
done < upstream/lua-files.txt
install -m 0644 upstream-payload/upstream-zapret2.commit zapret2/upstream-zapret2.commit
install -m 0644 upstream-payload/upstream-zapret2.release zapret2/upstream-zapret2.release
install -m 0644 upstream-payload/upstream-zapret2.archive.sha256 zapret2/upstream-zapret2.archive.sha256
- name: Validate shell syntax and security contracts
run: |
set -euo pipefail
bash -n upstream/fetch-release.sh
bash tests/release/release-channel.sh
chmod +x tests/shell/run.sh
sh tests/shell/run.sh
prepare-version:
name: Prepare Version
runs-on: android
outputs:
version: ${{ steps.version.outputs.version }}
version_code: ${{ steps.version.outputs.version_code }}
version_tag: ${{ steps.version.outputs.version_tag }}
steps:
- name: Checkout
uses: https://data.forgejo.org/actions/checkout@v6
- name: Read canonical release version
id: version
run: |
set -euo pipefail
VERSION_METADATA=$(sh tools/release-version.sh)
VERSION=$(sed -n 's/^version=//p' <<<"$VERSION_METADATA")
VERSION_CODE=$(sed -n 's/^version_code=//p' <<<"$VERSION_METADATA")
VERSION_TAG=$(sed -n 's/^version_tag=//p' <<<"$VERSION_METADATA")
[[ -n "$VERSION" && -n "$VERSION_CODE" && -n "$VERSION_TAG" ]]
printf '%s\n' "$VERSION_METADATA" >> "$FORGEJO_OUTPUT"
echo "Validating ${VERSION_TAG} (versionCode=${VERSION_CODE})"
build-apk:
name: Build Android APK
runs-on: android
needs: [prepare-version, shell-tests]
steps:
- name: Checkout
uses: https://data.forgejo.org/actions/checkout@v6
- name: Run Android unit tests
run: |
chmod +x gradlew
./gradlew testDebugUnitTest --dependency-verification=strict --no-daemon --stacktrace
working-directory: android-app
- name: Build Debug APK
run: ./gradlew assembleDebug --dependency-verification=strict --no-daemon --stacktrace
working-directory: android-app
- name: Run Android lint
run: ./gradlew lintRelease --dependency-verification=strict --no-daemon --stacktrace
working-directory: android-app
- name: Create keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
set -euo pipefail
for name in KEYSTORE_BASE64 KEYSTORE_PASSWORD KEY_ALIAS KEY_PASSWORD; do
[ -n "${!name:-}" ] || { echo "Required signing secret $name is missing" >&2; exit 1; }
done
umask 077
printf '%s' "$KEYSTORE_BASE64" | tr -d '[:space:]' | base64 --decode > keystore.jks
[ -s keystore.jks ] || { echo "Decoded keystore is empty" >&2; exit 1; }
chmod 0600 keystore.jks
keytool -list -keystore keystore.jks -storepass "$KEYSTORE_PASSWORD" -alias "$KEY_ALIAS" >/dev/null
working-directory: android-app
- name: Set version from CI metadata
run: |
VERSION="${{ needs.prepare-version.outputs.version }}"
VERSION_CODE="${{ needs.prepare-version.outputs.version_code }}"
echo "Setting version to $VERSION (code: $VERSION_CODE)"
# Update build.gradle.kts only after source-policy tests have checked
# the canonical repository placeholders.
sed -i "s/versionCode = [0-9]*/versionCode = ${VERSION_CODE}/" app/build.gradle.kts
sed -i "s/versionName = \"[^\"]*\"/versionName = \"${VERSION}\"/" app/build.gradle.kts
# Verify changes
grep -E "versionCode|versionName" app/build.gradle.kts
working-directory: android-app
- name: Build Release APK
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
# Retry build up to 3 times (Maven Central sometimes returns 403)
for i in 1 2 3; do
echo "Build attempt $i..."
if ./gradlew assembleRelease --dependency-verification=strict --no-daemon; then
echo "Build succeeded on attempt $i"
break
fi
if [ $i -lt 3 ]; then
echo "Build failed, waiting 30s before retry..."
sleep 30
else
echo "Build failed after 3 attempts"
exit 1
fi
done
working-directory: android-app
- name: Verify APK signature and certificate
env:
APK_SIGNING_CERT_SHA256: ${{ secrets.APK_SIGNING_CERT_SHA256 }}
run: |
set -euo pipefail
[ -n "${APK_SIGNING_CERT_SHA256:-}" ] || { echo "Required APK_SIGNING_CERT_SHA256 secret is missing" >&2; exit 1; }
APK='app/build/outputs/apk/release/app-release.apk'
[ -s "$APK" ]
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -mindepth 2 -maxdepth 2 -type f -name apksigner -print | sort -V | tail -n 1)"
[ -x "$APKSIGNER" ] || { echo "apksigner was not found" >&2; exit 1; }
VERIFY_OUTPUT="$("$APKSIGNER" verify --verbose --print-certs "$APK")"
printf '%s\n' "$VERIFY_OUTPUT"
ACTUAL_CERT="$(
printf '%s\n' "$VERIFY_OUTPUT" |
awk '
/certificate SHA-256 digest:/ {
digest = $0
sub(/^.*certificate SHA-256 digest:[[:space:]]*/, "", digest)
gsub(/:|[[:space:]]/, "", digest)
print toupper(digest)
}
' |
sort -u
)"
EXPECTED_CERT="$(printf '%s' "$APK_SIGNING_CERT_SHA256" | tr -d ':[:space:]' | tr '[:lower:]' '[:upper:]')"
[ "${#ACTUAL_CERT}" -eq 64 ] || { echo "Unable to read APK signer certificate digest" >&2; exit 1; }
[ "$ACTUAL_CERT" = "$EXPECTED_CERT" ] || { echo "APK signer certificate digest does not match the release identity" >&2; exit 1; }
(
cd "$(dirname "$APK")"
sha256sum "$(basename "$APK")" > "$(basename "$APK").sha256"
sha256sum -c "$(basename "$APK").sha256"
)
working-directory: android-app
- name: Remove private signing material
if: always()
run: find . -maxdepth 1 -type f -name keystore.jks -delete
working-directory: android-app
- name: Upload APK
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
name: zapret2-apk
path: |
android-app/app/build/outputs/apk/release/app-release.apk
android-app/app/build/outputs/apk/release/app-release.apk.sha256
fetch-upstream:
name: Fetch latest upstream zapret2 release
runs-on: android
outputs:
sha: ${{ steps.upstream.outputs.sha }}
tag: ${{ steps.upstream.outputs.tag }}
archive_sha256: ${{ steps.upstream.outputs.archive_sha256 }}
steps:
- name: Checkout module
uses: https://data.forgejo.org/actions/checkout@v6
- name: Fetch and verify upstream payload
id: upstream
run: |
set -euo pipefail
chmod +x upstream/fetch-release.sh
upstream/fetch-release.sh "$FORGEJO_WORKSPACE/upstream-payload"
- name: Upload verified upstream payload
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
name: upstream-zapret2-payload
path: upstream-payload
if-no-files-found: error
retention-days: 1
package-module:
name: Package Magisk Module
runs-on: android
needs: [prepare-version, fetch-upstream, build-apk]
steps:
- name: Checkout
uses: https://data.forgejo.org/actions/checkout@v6
- name: Download verified upstream payload
uses: https://data.forgejo.org/forgejo/download-artifact@v4
with:
name: upstream-zapret2-payload
path: upstream-payload
- name: Download APK
uses: https://data.forgejo.org/forgejo/download-artifact@v4
with:
name: zapret2-apk
path: apk-artifact
- name: Prepare module
env:
UPSTREAM_ZAPRET2_SHA: ${{ needs.fetch-upstream.outputs.sha }}
UPSTREAM_ZAPRET2_TAG: ${{ needs.fetch-upstream.outputs.tag }}
UPSTREAM_ZAPRET2_ARCHIVE_SHA256: ${{ needs.fetch-upstream.outputs.archive_sha256 }}
run: |
set -euo pipefail
VERSION="${{ needs.prepare-version.outputs.version }}"
VERSION_CODE="${{ needs.prepare-version.outputs.version_code }}"
[[ "$UPSTREAM_ZAPRET2_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$UPSTREAM_ZAPRET2_TAG" =~ ^v[0-9] ]]
grep -Fxq "$UPSTREAM_ZAPRET2_SHA" upstream-payload/upstream-zapret2.commit
grep -Fxq "$UPSTREAM_ZAPRET2_TAG" upstream-payload/upstream-zapret2.release
grep -Fxq "$UPSTREAM_ZAPRET2_ARCHIVE_SHA256" upstream-payload/upstream-zapret2.archive.sha256
# Create bin directories (ARM only, no x86 emulator support)
mkdir -p zapret2/bin/arm64-v8a
mkdir -p zapret2/bin/armeabi-v7a
# Copy the verified upstream release binaries.
install -m 0755 upstream-payload/bin/arm64-v8a/nfqws2 zapret2/bin/arm64-v8a/nfqws2
install -m 0755 upstream-payload/bin/armeabi-v7a/nfqws2 zapret2/bin/armeabi-v7a/nfqws2
test -s zapret2/bin/arm64-v8a/nfqws2
test -s zapret2/bin/armeabi-v7a/nfqws2
# Sync all official Lua files from the same release as the binaries.
# Custom module Lua files (custom_funcs.lua, zapret-multishake.lua,
# zapret-16kb.lua, zapret-wgobfs.lua) are kept from the module repo.
UPSTREAM_LUA="upstream-payload/lua"
test -d "$UPSTREAM_LUA"
while IFS= read -r upstream_lua || [ -n "$upstream_lua" ]; do
[ -z "$upstream_lua" ] && continue
test -f "$UPSTREAM_LUA/$upstream_lua"
install -m 0644 "$UPSTREAM_LUA/$upstream_lua" "zapret2/lua/$upstream_lua"
test -s "zapret2/lua/$upstream_lua"
echo "Synced upstream $upstream_lua"
done < upstream/lua-files.txt
install -m 0644 upstream-payload/upstream-zapret2.commit zapret2/upstream-zapret2.commit
install -m 0644 upstream-payload/upstream-zapret2.release zapret2/upstream-zapret2.release
install -m 0644 upstream-payload/upstream-zapret2.archive.sha256 zapret2/upstream-zapret2.archive.sha256
grep 'NFQWS2_COMPAT_VER' "zapret2/lua/zapret-lib.lua" | head -2
# Set module version from CI metadata
sed -i "s/^version=.*/version=v${VERSION}/" module.prop
sed -i "s/^versionCode=.*/versionCode=${VERSION_CODE}/" module.prop
sed -i "s|^updateJson=.*|updateJson=https://git.zapret.moe/zapretdiscordyoutube/magisk-zapret2/releases/latest/download/update.json|" module.prop
# Validate custom Lua files against upstream API
echo "Validating custom Lua compatibility with upstream..."
UPSTREAM_CORE="zapret2/lua/zapret-lib.lua zapret2/lua/zapret-antidpi.lua zapret2/lua/zapret-auto.lua"
CUSTOM_LUA="zapret2/lua/custom_funcs.lua zapret2/lua/zapret-multishake.lua zapret2/lua/zapret-16kb.lua zapret2/lua/zapret-wgobfs.lua"
WARN_COUNT=0
# Extract all "function name(" definitions from upstream core files
UPSTREAM_FUNCS=$(grep -h '^function [a-zA-Z_][a-zA-Z0-9_]*(' $UPSTREAM_CORE 2>/dev/null | sed 's/function \([a-zA-Z_][a-zA-Z0-9_]*\)(.*/\1/' | sort -u)
# Include globals/constants and nfqws2 C bindings used by custom Lua.
UPSTREAM_GLOBALS="DLOG b_debug VERDICT_PASS VERDICT_DROP HEXDUMP_DLOG_MAX resolve_range instance_cutoff"
ALL_UPSTREAM="$UPSTREAM_FUNCS $UPSTREAM_GLOBALS"
# Check every explicit custom-Lua required-functions declaration.
# Files without this optional declaration intentionally produce an
# empty set; that is not a grep/pipeline failure under pipefail.
for lua_file in $CUSTOM_LUA; do
[ -f "$lua_file" ] || continue
BASENAME=$(basename "$lua_file")
REQUIRED=$(awk '
/local required[[:space:]]*=[[:space:]]*{/ { capture=1 }
capture {
line=$0
while (match(line, /"[a-zA-Z_][a-zA-Z0-9_]*"/)) {
print substr(line, RSTART + 1, RLENGTH - 2)
line=substr(line, RSTART + RLENGTH)
}
if ($0 ~ /^[[:space:]]*}[,]?[[:space:]]*$/) exit
}
' "$lua_file" | sort -u)
for func in $REQUIRED; do
if ! echo "$ALL_UPSTREAM" | tr ' ' '\n' | grep -qx "$func"; then
echo "::error file=$lua_file::Function '$func' required by $BASENAME not found in upstream core Lua files"
WARN_COUNT=$((WARN_COUNT + 1))
fi
done
done
if [ "$WARN_COUNT" -gt 0 ]; then
echo "::error::$WARN_COUNT upstream API compatibility errors found"
exit 1
else
echo "All custom Lua files compatible with upstream API"
fi
# Make scripts executable
chmod +x customize.sh service.sh post-fs-data.sh uninstall.sh action.sh
chmod +x zapret2/scripts/*.sh
chmod +x zapret2/scripts/lifecycle/*.sh
chmod +x system/bin/zapret2-start system/bin/zapret2-stop system/bin/zapret2-status system/bin/zapret2-restart system/bin/zapret2-full-rollback
test "$(sed -n '2p' zapret2/runtime-manifest.tsv | tr -d '\r')" = 'owner_protocol|7|zapret2-firewall'
. ./zapret2/scripts/package-contract.sh
package_contract_apply_modes "$PWD" package
# Runtime validators intentionally require root-owned installed files.
# Exercise them against a private root-owned package copy, not the
# developer-owned Actions checkout.
PACKAGE_AUDIT_ROOT="$(mktemp -d)"
package_contract_assemble_package "$PWD" "$PACKAGE_AUDIT_ROOT"
chown -R 0:0 "$PACKAGE_AUDIT_ROOT"
find "$PACKAGE_AUDIT_ROOT" -type d -exec chmod 0755 {} +
package_contract_validate_all "$PACKAGE_AUDIT_ROOT" package
package_contract_validate_modes "$PACKAGE_AUDIT_ROOT" package
find "$PACKAGE_AUDIT_ROOT" -depth -delete
- name: Create ZIP
env:
UPSTREAM_ZAPRET2_SHA: ${{ needs.fetch-upstream.outputs.sha }}
UPSTREAM_ZAPRET2_TAG: ${{ needs.fetch-upstream.outputs.tag }}
UPSTREAM_ZAPRET2_ARCHIVE_SHA256: ${{ needs.fetch-upstream.outputs.archive_sha256 }}
run: |
set -eo pipefail
for provenance in UPSTREAM_ZAPRET2_SHA UPSTREAM_ZAPRET2_TAG UPSTREAM_ZAPRET2_ARCHIVE_SHA256; do
if [ -z "${!provenance:-}" ]; then
echo "Required upstream provenance value $provenance is missing" >&2
exit 1
fi
done
package_contract_step() {
local action="$1"
shift
PACKAGE_CONTRACT_CODE=""
PACKAGE_CONTRACT_DETAIL=""
"$action" "$@" && return 0
echo "Package contract check $action failed: ${PACKAGE_CONTRACT_CODE:-UNKNOWN} ${PACKAGE_CONTRACT_DETAIL:-}" >&2
return 1
}
VERSION="${{ needs.prepare-version.outputs.version }}"
VERSION_TAG="${{ needs.prepare-version.outputs.version_tag }}"
VERSION_CODE="${{ needs.prepare-version.outputs.version_code }}"
ZIP_NAME="zapret2-magisk-${VERSION_TAG}.zip"
# Update update.json with current version and direct asset URL
sed -i "s/\"version\": \"v[^\"]*\"/\"version\": \"${VERSION_TAG}\"/" update.json
sed -i "s/\"versionCode\": [0-9]*/\"versionCode\": ${VERSION_CODE}/" update.json
sed -i "s|\"zipUrl\": \"[^\"]*\"|\"zipUrl\": \"https://git.zapret.moe/zapretdiscordyoutube/magisk-zapret2/releases/download/${VERSION_TAG}/${ZIP_NAME}\"|" update.json
. ./zapret2/scripts/package-contract.sh
PACKAGE_ASSEMBLY_ROOT="$(mktemp -d)"
package_contract_step package_contract_assemble_package "$PWD" "$PACKAGE_ASSEMBLY_ROOT"
find . -maxdepth 1 -type f -name "$ZIP_NAME" -delete
(
cd "$PACKAGE_ASSEMBLY_ROOT"
# Whole assembly root: it already contains exactly the manifest.
zip -r "$FORGEJO_WORKSPACE/$ZIP_NAME" .
)
# Print and assert the distributable layout, file type, mode, and
# exact wrapper bytes so packaging cannot silently omit system/bin.
ZIP_LIST="$(mktemp)"
zipinfo -1 "$ZIP_NAME" | tee "$ZIP_LIST"
PACKAGE_VALIDATE_ROOT="$(mktemp -d)"
package_contract_step package_contract_validate_zip_names "$PWD" "$ZIP_LIST"
unzip -q "$ZIP_NAME" -d "$PACKAGE_VALIDATE_ROOT"
chown -R 0:0 "$PACKAGE_VALIDATE_ROOT"
find "$PACKAGE_VALIDATE_ROOT" -type d -exec chmod 0755 {} +
package_contract_step package_contract_validate_all "$PACKAGE_VALIDATE_ROOT" package
package_contract_step package_contract_validate_exact_tree "$PACKAGE_VALIDATE_ROOT" package
package_contract_step package_contract_validate_modes "$PACKAGE_VALIDATE_ROOT" package
if grep -Eq '^META-INF(/|$)' "$ZIP_LIST"; then
echo "Recovery flashing metadata must not be published" >&2
exit 1
fi
WRAPPER_EXPECTED="$(mktemp)"
WRAPPER_ACTUAL="$(mktemp)"
verify_provenance_entry() {
local entry="$1" expected="$2"
if [ "$(grep -Fxc "$entry" "$ZIP_LIST")" -ne 1 ]; then
echo "Provenance entry $entry is missing from the published ZIP" >&2
return 1
fi
printf '%s\n' "$expected" > "$WRAPPER_EXPECTED"
unzip -p "$ZIP_NAME" "$entry" > "$WRAPPER_ACTUAL"
if ! cmp -s "$WRAPPER_EXPECTED" "$WRAPPER_ACTUAL"; then
echo "Provenance entry $entry does not match the verified upstream value" >&2
return 1
fi
}
verify_provenance_entry 'zapret2/upstream-zapret2.commit' "$UPSTREAM_ZAPRET2_SHA"
verify_provenance_entry 'zapret2/upstream-zapret2.release' "$UPSTREAM_ZAPRET2_TAG"
verify_provenance_entry 'zapret2/upstream-zapret2.archive.sha256' "$UPSTREAM_ZAPRET2_ARCHIVE_SHA256"
find "$ZIP_LIST" "$WRAPPER_EXPECTED" "$WRAPPER_ACTUAL" -maxdepth 0 -type f -delete
find "$PACKAGE_ASSEMBLY_ROOT" -depth -delete
find "$PACKAGE_VALIDATE_ROOT" -depth -delete
sha256sum "$ZIP_NAME" > "$ZIP_NAME.sha256"
sha256sum -c "$ZIP_NAME.sha256"
# Rename APK with version
(
cd apk-artifact
sha256sum -c app-release.apk.sha256
)
cp apk-artifact/app-release.apk zapret2-control-${VERSION_TAG}.apk
sha256sum zapret2-control-${VERSION_TAG}.apk > zapret2-control-${VERSION_TAG}.apk.sha256
sha256sum -c zapret2-control-${VERSION_TAG}.apk.sha256
- name: Upload module
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
name: zapret2-magisk-module
path: |
zapret2-magisk-*.zip
zapret2-magisk-*.zip.sha256
- name: Upload APK artifact
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
name: zapret2-control-apk
path: |
zapret2-control-*.apk
zapret2-control-*.apk.sha256
- name: Upload release metadata
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
name: zapret2-release-metadata
path: update.json