Files
backspace/ops/release-android.sh
devsyncwrldandClaude Opus 5 9b7511f8bf
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
chore(ops): version the release scripts
They existed only in /opt/ops on the VPS — no history, no backup. Rebuilding
that machine would have taken the whole publishing process with it, including
the Android signing flow written today.

/opt/ops now symlinks here so there is one copy rather than two that drift.
No secrets move: the scripts read tokens and the signing key from paths under
/root that stay on the VPS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:21:00 -03:00

121 lines
5.1 KiB
Bash
Executable File

#!/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"