magisk-zapret2/.github/workflows/build.yml
loop-uh e5263f0125 Restore upstream provenance verification inputs
Create ZIP verified the release tag and archive digest against environment
variables the step never received, so packaging failed with an empty
comparison and no diagnostic output. Pass the fetch-upstream outputs to the
step, refuse to run with a missing provenance value, and report the package
contract code when a validator rejects the ZIP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 01:24:49 +03:00

494 lines
22 KiB
YAML

name: Background release validation
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
shell-tests:
name: Shell integration tests
runs-on: ubuntu-latest
needs: fetch-upstream
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Validate owned source hygiene
run: |
set -euo pipefail
EMPTY_TREE=4b825dc642cb6eb9a060e54bf8d69288fbee4904
git diff --check "$EMPTY_TREE" "$GITHUB_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: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
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
sudo sh tests/shell/run.sh
prepare-version:
name: Prepare Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
version_code: ${{ steps.version.outputs.version_code }}
version_tag: ${{ steps.version.outputs.version_tag }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- 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" >> "$GITHUB_OUTPUT"
echo "Validating ${VERSION_TAG} (versionCode=${VERSION_CODE})"
build-apk:
name: Build Android APK
runs-on: ubuntu-latest
needs: [prepare-version, shell-tests]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up JDK 17
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d # v4
- 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: rm -f keystore.jks
working-directory: android-app
- name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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: ubuntu-latest
outputs:
sha: ${{ steps.upstream.outputs.sha }}
tag: ${{ steps.upstream.outputs.tag }}
archive_sha256: ${{ steps.upstream.outputs.archive_sha256 }}
steps:
- name: Checkout module
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Fetch and verify upstream payload
id: upstream
run: |
set -euo pipefail
chmod +x upstream/fetch-release.sh
upstream/fetch-release.sh "$GITHUB_WORKSPACE/upstream-payload"
- name: Upload verified upstream payload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: upstream-zapret2-payload
path: upstream-payload
if-no-files-found: error
retention-days: 1
package-module:
name: Package Magisk Module
runs-on: ubuntu-latest
needs: [prepare-version, fetch-upstream, build-apk]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Download verified upstream payload
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: upstream-zapret2-payload
path: upstream-payload
- name: Download APK
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
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://github.com/youtubediscord/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 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"
sudo chown -R 0:0 "$PACKAGE_AUDIT_ROOT"
sudo 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
sudo rm -rf "$PACKAGE_AUDIT_ROOT"
- 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://github.com/youtubediscord/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"
rm -f "$ZIP_NAME"
(
cd "$PACKAGE_ASSEMBLY_ROOT"
zip -r "$GITHUB_WORKSPACE/$ZIP_NAME" \
module.prop customize.sh service.sh uninstall.sh action.sh system zapret2
)
# 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"
sudo chown -R 0:0 "$PACKAGE_VALIDATE_ROOT"
sudo 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"
rm -f "$ZIP_LIST" "$WRAPPER_EXPECTED" "$WRAPPER_ACTUAL"
rm -rf "$PACKAGE_ASSEMBLY_ROOT"
sudo rm -rf "$PACKAGE_VALIDATE_ROOT"
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: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: zapret2-magisk-module
path: |
zapret2-magisk-*.zip
zapret2-magisk-*.zip.sha256
- name: Upload APK artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: zapret2-control-apk
path: |
zapret2-control-*.apk
zapret2-control-*.apk.sha256
- name: Upload release metadata
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: zapret2-release-metadata
path: update.json