ZapretKVN-android/scripts/create-release-bundle.sh
loop-uh 9ffd73a6db
Some checks failed
Android CI / build (push) Has been cancelled
release: prepare Android 0.5.7 — быстрое подключение и незаметные проверки
- core-release-freeze.json: пара Windows 0.8.32 / Android v0.5.7, ядра прежние.
- release-changes.txt: пять пунктов про скорость, Hysteria2, DNS и незаметность.
- create-release-bundle.sh: проверки на устройстве/эмуляторе из
  release-device-evidence.txt попадают в заметки только для своего тега,
  иначе остаётся «ADB device tests not verified».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:07:49 +03:00

205 lines
10 KiB
Shell
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TAG="${1:?release tag is required}"
SOURCE_DIR="${2:?directory containing the signed release APK matrix is required}"
OUTPUT_DIR="${3:-$PROJECT_ROOT/release-out}"
RELEASE_ABIS=(arm64-v8a armeabi-v7a x86_64)
# Single source of truth for the user-facing changelog, shared with the Telegram
# announcement in publish-local-stable.sh.
RELEASE_CHANGES_FILE="${ZAPRET_RELEASE_CHANGES_FILE:-$PROJECT_ROOT/scripts/release-changes.txt}"
# Optional record of the ADB/device checks actually run for THIS tag. Its lines are used
# only when the first non-comment line is exactly "tag: <TAG>"; otherwise (absent file or
# a stale tag) the notes keep the default "ADB device tests not verified" attestation.
RELEASE_DEVICE_EVIDENCE_FILE="${ZAPRET_RELEASE_DEVICE_EVIDENCE_FILE:-$PROJECT_ROOT/scripts/release-device-evidence.txt}"
[[ -f "$RELEASE_CHANGES_FILE" ]] || {
echo "Missing changelog source: $RELEASE_CHANGES_FILE" >&2
exit 1
}
# shellcheck disable=SC1090,SC1091
source "$PROJECT_ROOT/core.properties"
while IFS='=' read -r name value; do
case "$name" in
ZAPRET_VERSION_NAME|ZAPRET_VERSION_CODE|ZAPRET_PRERELEASE|ZAPRET_RELEASE_CHANNEL)
printf -v "$name" '%s' "$value"
;;
*)
echo "Unexpected release variable: $name" >&2
exit 1
;;
esac
done < <("$PROJECT_ROOT/scripts/derive-release-version.sh" "$TAG")
if [[ -z "${ANDROID_HOME:-}" && -f "$PROJECT_ROOT/local.properties" ]]; then
ANDROID_HOME="$(sed -n 's/^sdk\.dir=//p' "$PROJECT_ROOT/local.properties" | tail -n 1)"
export ANDROID_HOME
fi
: "${ANDROID_HOME:?ANDROID_HOME must point to an installed Android SDK}"
AAPT2="$ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS/aapt2"
APKSIGNER="$ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS/apksigner"
for command in jq sha256sum unzip; do
command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
done
[[ -x "$AAPT2" && -x "$APKSIGNER" && -d "$SOURCE_DIR" ]]
if [[ -d "$OUTPUT_DIR" && -n "$(find "$OUTPUT_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
echo "Release output directory must be empty: $OUTPUT_DIR" >&2
exit 1
fi
mkdir -p "$OUTPUT_DIR"
ARTIFACTS=()
SIGNER_SHA256=""
ARM64_APK_NAME=""
ARM64_APK_SHA256=""
ARM64_APK_SIZE=""
for abi in "${RELEASE_ABIS[@]}"; do
source_apk="$SOURCE_DIR/app-$abi-release.apk"
[[ -f "$source_apk" ]]
mapfile -t native_abis < <(
unzip -Z1 "$source_apk" \
| sed -n 's#^lib/\([^/]*\)/.*\.so$#\1#p' \
| sort -u
)
if [[ "${#native_abis[@]}" -ne 1 || "${native_abis[0]}" != "$abi" ]]; then
echo "Unexpected native ABI set in $source_apk: ${native_abis[*]:-none}" >&2
exit 1
fi
unzip -Z1 "$source_apk" | grep -Fx "lib/$abi/libbox.so" >/dev/null
signature_info="$($APKSIGNER verify --verbose --print-certs "$source_apk")"
mapfile -t signer_digests < <(
sed -n 's/^Signer #[0-9][0-9]* certificate SHA-256 digest: //p' <<<"$signature_info"
)
[[ "${#signer_digests[@]}" -eq 1 ]]
signer="$(tr '[:upper:]' '[:lower:]' <<<"${signer_digests[0]}" | tr -d ':[:space:]')"
[[ "$signer" =~ ^[0-9a-f]{64}$ ]]
if [[ -z "$SIGNER_SHA256" ]]; then
SIGNER_SHA256="$signer"
elif [[ "$signer" != "$SIGNER_SHA256" ]]; then
echo "Release APKs are not signed by the same certificate" >&2
exit 1
fi
badging="$($AAPT2 dump badging "$source_apk" | sed -n '1p')"
[[ "$badging" == *"name='io.github.zapretkvn.android'"* ]]
[[ "$badging" == *"versionCode='$ZAPRET_VERSION_CODE'"* ]]
[[ "$badging" == *"versionName='$ZAPRET_VERSION_NAME'"* ]]
apk_name="Zapret-KVN-$TAG-$abi.apk"
cp "$source_apk" "$OUTPUT_DIR/$apk_name"
apk_sha256="$(sha256sum "$OUTPUT_DIR/$apk_name" | awk '{print $1}')"
apk_size="$(stat -c '%s' "$OUTPUT_DIR/$apk_name")"
printf '%s %s\n' "$apk_sha256" "$apk_name" > "$OUTPUT_DIR/$apk_name.sha256"
ARTIFACTS+=("$(
jq -cn \
--arg abi "$abi" \
--arg apk_file "$apk_name" \
--arg apk_sha256 "$apk_sha256" \
--argjson apk_size "$apk_size" \
'{abi:$abi,apk_file:$apk_file,apk_sha256:$apk_sha256,apk_size:$apk_size}'
)")
if [[ "$abi" == arm64-v8a ]]; then
ARM64_APK_NAME="$apk_name"
ARM64_APK_SHA256="$apk_sha256"
ARM64_APK_SIZE="$apk_size"
fi
done
if [[ -n "${ZAPRET_EXPECTED_SIGNER_SHA256:-}" ]]; then
EXPECTED_SIGNER="$(tr '[:upper:]' '[:lower:]' <<<"$ZAPRET_EXPECTED_SIGNER_SHA256" | tr -d ':[:space:]')"
[[ "$EXPECTED_SIGNER" =~ ^[0-9a-f]{64}$ && "$SIGNER_SHA256" == "$EXPECTED_SIGNER" ]]
fi
# Schema 2 is used by architecture-aware clients.
printf '%s\n' "${ARTIFACTS[@]}" | jq -s \
--arg version_name "$ZAPRET_VERSION_NAME" \
--argjson version_code "$ZAPRET_VERSION_CODE" \
--arg application_id "io.github.zapretkvn.android" \
--arg core_tag "$CORE_TAG" \
--arg core_commit "$CORE_COMMIT" \
--arg core_patch_sha256 "$CORE_PATCH_SHA256" \
--arg signer_sha256 "$SIGNER_SHA256" \
'{schema:2,version_name:$version_name,version_code:$version_code,application_id:$application_id,core_tag:$core_tag,core_commit:$core_commit,core_patch_sha256:$core_patch_sha256,signer_sha256:$signer_sha256,artifacts:.}' \
> "$OUTPUT_DIR/release-metadata-v2.json"
# Keep the schema-1 arm64 manifest so already-installed arm64 versions can cross
# the metadata transition through their existing updater.
jq -n \
--arg version_name "$ZAPRET_VERSION_NAME" \
--argjson version_code "$ZAPRET_VERSION_CODE" \
--arg application_id "io.github.zapretkvn.android" \
--arg core_tag "$CORE_TAG" \
--arg core_commit "$CORE_COMMIT" \
--arg core_patch_sha256 "$CORE_PATCH_SHA256" \
--arg signer_sha256 "$SIGNER_SHA256" \
--arg apk_file "$ARM64_APK_NAME" \
--arg apk_sha256 "$ARM64_APK_SHA256" \
--argjson apk_size "$ARM64_APK_SIZE" \
'{schema:1,version_name:$version_name,version_code:$version_code,application_id:$application_id,core_tag:$core_tag,core_commit:$core_commit,core_patch_sha256:$core_patch_sha256,signer_sha256:$signer_sha256,abi:["arm64-v8a"],apk_file:$apk_file,apk_sha256:$apk_sha256,apk_size:$apk_size}' \
> "$OUTPUT_DIR/release-metadata.json"
{
printf '%s\n' \
"## Zapret KVN $ZAPRET_VERSION_NAME" \
'' \
"- Канал: $ZAPRET_RELEASE_CHANNEL" \
'- Android package: io.github.zapretkvn.android' \
'- APK: отдельные arm64-v8a, armeabi-v7a и x86_64' \
"- Core tag: \`$CORE_TAG\`" \
"- Core commit: \`$CORE_COMMIT\`" \
"- Core patch SHA-256: \`$CORE_PATCH_SHA256\`" \
"- Signing certificate SHA-256: \`$SIGNER_SHA256\`"
for artifact in "${ARTIFACTS[@]}"; do
printf -- '- %s APK SHA-256: `%s`\n' \
"$(jq -r .abi <<<"$artifact")" \
"$(jq -r .apk_sha256 <<<"$artifact")"
done
printf '%s\n' \
'' \
'### Что изменилось' \
''
while IFS= read -r change_line || [[ -n "$change_line" ]]; do
[[ -z "${change_line// }" || "$change_line" == \#* ]] && continue
printf -- '- %s\n' "${change_line# }"
done < "$RELEASE_CHANGES_FILE"
printf '%s\n' \
'' \
'### Проверка релиза' \
'' \
'- Пройдены host/unit/integration-проверки, общий WG/AWG JSON/UAPI golden-набор и проверки UDP для обеих версий sing-box; Android instrumentation APK собран.'
device_evidence=()
if [[ -f "$RELEASE_DEVICE_EVIDENCE_FILE" ]]; then
mapfile -t device_evidence < <(grep -v '^[[:space:]]*\(#\|$\)' "$RELEASE_DEVICE_EVIDENCE_FILE" || true)
fi
if (( ${#device_evidence[@]} > 1 )) && [[ "${device_evidence[0]}" == "tag: $TAG" ]]; then
for evidence_line in "${device_evidence[@]:1}"; do
printf -- '- %s\n' "$evidence_line"
done
else
printf '%s\n' \
'- Release attestation: host-verified; Android ADB device tests not verified.' \
'- Эмулятор и физическое Android-устройство, включая Gecko на реальной сети, для этого релиза не проверялись; этот дополнительный ADB-gate необязателен.'
fi
printf '%s\n' \
'' \
'Каждый APK содержит только одну архитектуру и встроенный libbox; отдельная динамическая загрузка ядра не используется.' \
'' \
'### Известные ограничения MVP' \
'' \
'- Android 8+; доступны отдельные arm64-v8a, armeabi-v7a и x86_64 APK, интерфейс рассчитан на телефоны.' \
'- Always-on/Lockdown и shared UID не поддерживаются как гарантированный per-app режим.' \
'- Managed DNS перехватывает TCP/UDP 53, но не встроенный DoH, DoT или mDNS; DoT на TCP 853 отклоняется; FakeIP выключен.' \
'- Domain-only block не является firewall: для гарантии нужен IP/CIDR rule-set.' \
'- Явный outbound tag общего правила должен существовать в каждом используемом профиле; пустой tag автоматически выбирает proxy текущего профиля.' \
'- Clash YAML и Hysteria v1 URI пока не импортируются; raw JSON остаётся ответственностью пользователя.' \
'- Обновления проверяются в фоне (при запуске и раз в сутки) и скачиваются сами, по возможности только изменившимися частями; на Android 12+ установка проходит без подтверждения, когда приложение уже ставило себя, иначе silent install недоступен и нужно одно нажатие. Подписки не обновляются в фоне, core обновляется только вместе с APK.' \
'- При неработающем proxy/DNS VPN закрывается без бесконечного retry и plaintext DNS fallback.'
} > "$OUTPUT_DIR/RELEASE_NOTES.md"
echo "Release bundle created in $OUTPUT_DIR"