#!/usr/bin/env bash # Baixa o APK compilado no GitHub, assina aqui e publica no Gitea. # # A chave de assinatura fica nesta VPS e nao sobe para o GitHub. Ela e o que # prova que uma atualizacao veio de nos: um APK assinado com outra chave nao # instala por cima do instalado, o Android recusa. Por isso o runner entrega o # APK sem assinatura e a assinatura acontece aqui. # # Uso: release-android.sh [run_id] # sem run_id, dispara um build novo e espera terminar. set -euo pipefail REPO=${REPO:-/opt/backspace} GH_REPO=${GH_REPO:-syncwrld/resenhacord} TOOLS=/opt/ops/android-tools/build-tools KEYSTORE=/root/android-signing/resenhacord.keystore PASSFILE=/root/android-signing/password.txt OUT=/opt/ops/android-out GITEA_API=https://git.resenha.website/api/v1/repos/devsyncwrld/backspace TOKEN=$(cat /root/.github-token) gh() { curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.github+json" "$@"; } # apksigner e um script que chama java, e esta VPS nao tem JDK instalado (nem # deve ter: ela roda a producao, nao compila nada). Roda num container, com as # ferramentas, a chave e a pasta de saida montadas. zipalign e binario nativo e # roda direto no host. apksigner() { docker run --rm \ -v "$TOOLS":/tools:ro \ -v /root/android-signing:/ks:ro \ -v "$OUT":/out \ eclipse-temurin:21-jdk /tools/apksigner "$@" } for f in "$TOOLS/apksigner" "$TOOLS/zipalign" "$KEYSTORE" "$PASSFILE"; do [ -e "$f" ] || { echo "faltando: $f" >&2; exit 1; } done RUN_ID=${1:-} if [ -z "$RUN_ID" ]; then echo "==> Disparando o build no GitHub" gh -o /dev/null -X POST \ "https://api.github.com/repos/$GH_REPO/actions/workflows/android.yml/dispatches" \ -d '{"ref":"main"}' sleep 10 RUN_ID=$(gh "https://api.github.com/repos/$GH_REPO/actions/workflows/android.yml/runs?per_page=1" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['workflow_runs'][0]['id'])") echo " run=$RUN_ID" echo "==> Esperando (uns 4 min)" for _ in $(seq 1 90); do read -r status conclusion < <(gh "https://api.github.com/repos/$GH_REPO/actions/runs/$RUN_ID" \ | python3 -c "import sys,json;r=json.load(sys.stdin);print(r['status'],r['conclusion'])") [ "$status" = "completed" ] && break sleep 20 done [ "${conclusion:-}" = "success" ] || { echo "build falhou: ${conclusion:-timeout}" >&2; exit 1; } fi echo "==> Baixando o APK sem assinatura" mkdir -p "$OUT" URL=$(gh "https://api.github.com/repos/$GH_REPO/actions/runs/$RUN_ID/artifacts" \ | python3 -c "import sys,json;a=json.load(sys.stdin)['artifacts'];print(a[0]['archive_download_url'] if a else '')") [ -n "$URL" ] || { echo "o run $RUN_ID nao tem artefato" >&2; exit 1; } curl -sL -H "Authorization: Bearer $TOKEN" "$URL" -o "$OUT/apk.zip" rm -f "$OUT/resenhacord-unsigned.apk" unzip -o -q "$OUT/apk.zip" -d "$OUT" VERSION=$(python3 -c "import json;print(json.load(open('$REPO/package.json'))['version'])") SIGNED="$OUT/Resenhacord-$VERSION.apk" echo "==> Alinhando e assinando $VERSION" # --key-pass omitido de proposito: sem ele o apksigner reusa a senha do # keystore, que e a mesma. Passar o mesmo arquivo nas duas opcoes falha — ele le # os arquivos de senha em sequencia e bate no fim do arquivo na segunda leitura. # zipalign antes do apksigner, nao depois: o alinhamento move bytes dentro do # zip e invalidaria a assinatura se viesse por ultimo. "$TOOLS/zipalign" -p -f 4 "$OUT/resenhacord-unsigned.apk" "$OUT/aligned.apk" apksigner sign \ --ks /ks/resenhacord.keystore --ks-pass file:/ks/password.txt \ --ks-key-alias resenhacord \ --out "/out/$(basename "$SIGNED")" /out/aligned.apk rm -f "$OUT/aligned.apk" "$OUT/apk.zip" echo "==> Conferindo a assinatura" apksigner verify --print-certs "/out/$(basename "$SIGNED")" | head -5 if [ "${1:-}" = "--no-publish" ] || [ "${PUBLISH:-1}" = "0" ]; then echo "APK pronto: $SIGNED" exit 0 fi echo "==> Publicando no Gitea" GITEA_TOKEN=$(cat /root/.gitea-token 2>/dev/null || true) if [ -z "$GITEA_TOKEN" ]; then echo "sem /root/.gitea-token — APK pronto localmente em $SIGNED" exit 0 fi api() { curl -s -w '\n%{http_code}' -X "$1" -H "Authorization: token $GITEA_TOKEN" "$GITEA_API$2" "${@:3}"; } body() { sed '$d' <<<"$1"; } code() { tail -n1 <<<"$1"; } R=$(api GET "/releases/tags/latest") ID=$(body "$R" | python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") [ -n "$ID" ] || { echo "release 'latest' nao encontrada no Gitea" >&2; exit 1; } # Remove o APK anterior: dois anexos com o mesmo nome fazem o Gitea servir o # mais antigo, e quem baixasse pegaria a versao velha sem erro nenhum. api GET "/releases/$ID/assets" | sed '$d' \ | python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin) if a['name'].endswith('.apk')]" \ | while read -r aid; do [ -n "$aid" ] && api DELETE "/releases/$ID/assets/$aid" >/dev/null && echo " removido anexo $aid" done NAME=$(basename "$SIGNED") R=$(api POST "/releases/$ID/assets?name=$NAME" -F "attachment=@$SIGNED") [ "$(code "$R")" = "201" ] || { echo "falha no upload — HTTP $(code "$R"): $(body "$R")" >&2; exit 1; } echo "publicado: $NAME" echo "https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/$NAME"