chore(ops): version the release scripts
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
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
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>
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
# Scripts de operação da instância
|
||||||
|
|
||||||
|
Rodam na VPS, em `/opt/ops`, onde os arquivos são links simbólicos para esta
|
||||||
|
pasta — o repositório é a única cópia de verdade. Antes disso eles existiam só
|
||||||
|
na VPS, sem versão nem backup: uma reinstalação do servidor levaria junto todo
|
||||||
|
o processo de publicação.
|
||||||
|
|
||||||
|
| script | o que faz |
|
||||||
|
| --- | --- |
|
||||||
|
| `call-check.sh` | Diz se tem alguém em call. Roda antes de reiniciar o servidor. |
|
||||||
|
| `release-desktop.sh` | Espelha o fork no GitHub e dispara a compilação dos instaladores de desktop. |
|
||||||
|
| `release-android.sh` | Compila o APK no GitHub, assina na VPS e publica no Gitea. |
|
||||||
|
|
||||||
|
## O que estes scripts leem e não está aqui
|
||||||
|
|
||||||
|
Nada de segredo entra no repositório. Os arquivos abaixo ficam só na VPS, com
|
||||||
|
permissão `600`:
|
||||||
|
|
||||||
|
- `/root/.github-token` — dispara e baixa os builds do GitHub.
|
||||||
|
- `/root/.gitea-token` — publica os anexos das releases.
|
||||||
|
- `/root/android-signing/` — a chave que assina o APK, mais a senha dela.
|
||||||
|
|
||||||
|
A chave do Android merece atenção especial: ela é o que prova ao Android que
|
||||||
|
uma atualização veio de nós. Se for perdida, ninguém consegue atualizar por
|
||||||
|
cima — todo mundo tem que desinstalar e instalar de novo, e não existe
|
||||||
|
recuperação. Mantenha uma cópia fora da VPS.
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Reports how many people are currently in a call on this instance.
|
||||||
|
#
|
||||||
|
# Asks LiveKit directly (ListRooms over its twirp API) rather than inferring
|
||||||
|
# from the app: LiveKit is the authority on who is actually connected to media.
|
||||||
|
# Prints the participant count and exits 0 when the instance is idle, 1 when
|
||||||
|
# someone is in a call — so it can gate a deploy.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ENV_FILE=${ENV_FILE:-/opt/backspace/.env}
|
||||||
|
# `|| true`: under `set -o pipefail` a grep with no match aborts the script
|
||||||
|
# before the check below, exiting 1 — which in this script's contract means
|
||||||
|
# "someone is in a call" rather than "I could not tell".
|
||||||
|
KEY=$(grep -E '^LIVEKIT_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
|
||||||
|
SECRET=$(grep -E '^LIVEKIT_API_SECRET=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
|
||||||
|
[ -n "$KEY" ] && [ -n "$SECRET" ] || { echo "LiveKit credentials not found in $ENV_FILE" >&2; exit 2; }
|
||||||
|
|
||||||
|
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
|
||||||
|
|
||||||
|
now=$(date +%s)
|
||||||
|
header=$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | b64url)
|
||||||
|
payload=$(printf '{"iss":"%s","sub":"%s","nbf":%d,"exp":%d,"video":{"roomList":true}}' \
|
||||||
|
"$KEY" "$KEY" "$now" "$((now + 60))" | b64url)
|
||||||
|
sig=$(printf '%s.%s' "$header" "$payload" \
|
||||||
|
| openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
|
||||||
|
jwt="$header.$payload.$sig"
|
||||||
|
|
||||||
|
resp=$(curl -sS --max-time 10 -w '\n%{http_code}' -X POST http://127.0.0.1:7880/twirp/livekit.RoomService/ListRooms \
|
||||||
|
-H "Authorization: Bearer $jwt" -H 'Content-Type: application/json' -d '{}') || {
|
||||||
|
echo "LiveKit unreachable — refusing to report idle" >&2; exit 2; }
|
||||||
|
|
||||||
|
code=$(printf '%s' "$resp" | tail -n1)
|
||||||
|
resp=$(printf '%s' "$resp" | sed '$d')
|
||||||
|
|
||||||
|
# A failed query returns no participant counts, which would read as "idle" and
|
||||||
|
# green-light a deploy on top of a live call. Anything but a well-formed 200 is
|
||||||
|
# treated as unknown, not as empty.
|
||||||
|
if [ "$code" != "200" ] || ! printf '%s' "$resp" | grep -q '"rooms"'; then
|
||||||
|
echo "LiveKit answered HTTP $code without a room list — status unknown, not deploying" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# numParticipants is omitted by protobuf JSON when zero, so absence means empty.
|
||||||
|
total=$(printf '%s' "$resp" | grep -oE '"numParticipants":[0-9]+' | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || true)
|
||||||
|
total=${total:-0}
|
||||||
|
|
||||||
|
if [ "$total" -gt 0 ]; then
|
||||||
|
echo "EM CALL: $total participante(s) — nao deployar"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OCIOSO: ninguem em call"
|
||||||
|
exit 0
|
||||||
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
#!/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"
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Espelha o fork no GitHub e dispara a compilacao dos instaladores.
|
||||||
|
#
|
||||||
|
# O GitHub e usado apenas como maquina de build: ele fornece runners Windows
|
||||||
|
# prontos, e o modulo nativo do audio (WASAPI) so compila la. O Gitea continua
|
||||||
|
# sendo o repositorio principal.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO=${REPO:-/opt/backspace}
|
||||||
|
TAG=${1:-}
|
||||||
|
cd "$REPO"
|
||||||
|
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo "uso: $0 vX.Y.Z" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `ssh -T git@github.com` always exits 1 (GitHub refuses shell access), and
|
||||||
|
# under `pipefail` that status wins over grep's — so piping straight into the
|
||||||
|
# `if` reported failure even on a working key. Capture, then test.
|
||||||
|
SSH_PROBE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 || true)"
|
||||||
|
if ! printf '%s' "$SSH_PROBE" | grep -q "successfully authenticated"; then
|
||||||
|
echo "GitHub ainda nao aceita a chave desta VPS." >&2
|
||||||
|
echo "Adicione esta chave em https://github.com/settings/keys :" >&2
|
||||||
|
cat ~/.ssh/id_ed25519.pub >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Espelhando main no GitHub"
|
||||||
|
git push github main
|
||||||
|
|
||||||
|
echo "==> Marcando $TAG (e a tag que dispara o workflow release.yml)"
|
||||||
|
git tag -f "$TAG"
|
||||||
|
git push -f github "$TAG"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Compilacao disparada. Acompanhe em:"
|
||||||
|
echo " https://github.com/syncwrld/resenhacord/actions"
|
||||||
|
echo "Os instaladores aparecem em Releases quando terminar (~15 min)."
|
||||||
Reference in New Issue
Block a user