Atualização pelo próprio app (Tauri updater) com cartão de aviso
Ao abrir, o app consulta o latest.json da release no GitHub; havendo versão nova, um cartão oferece Atualizar agora: baixa, confere a assinatura com a chave pública embutida, instala e reabre (AppImage substituído no lugar; Windows e macOS pelo instalador). Instalação por .deb ou .rpm só avisa. Configurações ganha a opção de desligar a consulta e o botão Verificar agora. A release assina os pacotes com a chave guardada no ambiente 'release' do GitHub (só tags v*) e o latest.json é montado de uma vez por scripts/make-latest-json.py, depois que todos os jobs de empacotamento terminam. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
17b0e89e03
commit
daa12ba117
20 files changed
+341
-7
No files matched your search
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Monta o latest.json que o atualizador do app lê (Tauri updater), a partir dos pacotes e das
|
||||
assinaturas `.sig` já enviados à release.
|
||||
|
||||
Roda no job de publicação, depois que todos os jobs de empacotamento terminaram. Montar o arquivo
|
||||
aqui, em um passo só, evita a corrida de quem tenta atualizá-lo em paralelo por plataforma.
|
||||
|
||||
Uso: make-latest-json.py <tag> <notas.md> <saida.json> (usa `gh` e GH_REPO/GITHUB_REPOSITORY)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# (chaves de plataforma, padrão do nome do pacote). {v} = versão sem o "v".
|
||||
PLATFORMS = [
|
||||
(["linux-x86_64", "linux-x86_64-appimage"], "Guara_{v}_amd64.AppImage"),
|
||||
(["windows-x86_64", "windows-x86_64-nsis"], "Guara_{v}_x64-setup.exe"),
|
||||
(["windows-x86_64-msi"], "Guara_{v}_x64_en-US.msi"),
|
||||
(["darwin-aarch64", "darwin-aarch64-app"], "Guara_{v}_aarch64.app.tar.gz"),
|
||||
(["darwin-x86_64", "darwin-x86_64-app"], "Guara_{v}_x64.app.tar.gz"),
|
||||
]
|
||||
|
||||
|
||||
def build(version, notes, assets, pub_date):
|
||||
"""`assets` = {nome_do_pacote: (url, assinatura)}. Devolve o dicionário do latest.json.
|
||||
|
||||
Plataforma sem pacote ou sem assinatura sai da lista; o chamador decide se isso é erro."""
|
||||
platforms = {}
|
||||
for keys, pattern in PLATFORMS:
|
||||
name = pattern.format(v=version)
|
||||
if name not in assets:
|
||||
continue
|
||||
url, sig = assets[name]
|
||||
for k in keys:
|
||||
platforms.setdefault(k, {"signature": sig.strip(), "url": url})
|
||||
return {"version": version, "notes": notes.strip(), "pub_date": pub_date, "platforms": platforms}
|
||||
|
||||
|
||||
def main():
|
||||
tag, notes_path, out = sys.argv[1:4]
|
||||
version = tag.lstrip("v")
|
||||
repo = os.environ.get("GH_REPO") or os.environ["GITHUB_REPOSITORY"]
|
||||
rel = json.loads(subprocess.check_output(["gh", "release", "view", tag, "--repo", repo, "--json", "assets"]))
|
||||
urls = {a["name"]: a["url"] for a in rel["assets"]}
|
||||
assets = {}
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
for _, pattern in PLATFORMS:
|
||||
name = pattern.format(v=version)
|
||||
if name in urls and name + ".sig" in urls:
|
||||
subprocess.check_call(["gh", "release", "download", tag, "--repo", repo, "-p", name + ".sig", "-D", d])
|
||||
assets[name] = (urls[name], open(os.path.join(d, name + ".sig")).read())
|
||||
data = build(version, open(notes_path, encoding="utf-8").read(), assets,
|
||||
datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
|
||||
# Sem o Linux (AppImage) o atualizador do dono do projeto não funciona: não publica pela metade.
|
||||
for must in ("linux-x86_64", "windows-x86_64", "darwin-aarch64", "darwin-x86_64"):
|
||||
if must not in data["platforms"]:
|
||||
sys.exit(f"faltam o pacote ou a assinatura de {must}; confira se TAURI_SIGNING_PRIVATE_KEY está nos segredos")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
print(f"{out}: {sorted(data['platforms'])}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Teste de scripts/make-latest-json.py: python3 scripts/test_make_latest_json.py"""
|
||||
import importlib.util
|
||||
import os
|
||||
import unittest
|
||||
|
||||
spec = importlib.util.spec_from_file_location("m", os.path.join(os.path.dirname(__file__), "make-latest-json.py"))
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
|
||||
BASE = "https://github.com/x/y/releases/download/v1.3.0/"
|
||||
|
||||
|
||||
def assets(version="1.3.0"):
|
||||
names = [p.format(v=version) for _, p in m.PLATFORMS]
|
||||
return {n: (BASE + n, f"assinatura-de-{n}\n") for n in names}
|
||||
|
||||
|
||||
class LatestJson(unittest.TestCase):
|
||||
def test_all_platforms_and_variants(self):
|
||||
d = m.build("1.3.0", "### Novidades\n", assets(), "2026-10-23T12:00:00Z")
|
||||
self.assertEqual(d["version"], "1.3.0")
|
||||
self.assertEqual(d["notes"], "### Novidades")
|
||||
p = d["platforms"]
|
||||
for k in ("linux-x86_64", "linux-x86_64-appimage", "windows-x86_64", "windows-x86_64-nsis",
|
||||
"windows-x86_64-msi", "darwin-aarch64", "darwin-x86_64"):
|
||||
self.assertIn(k, p)
|
||||
# a chave genérica do Windows aponta para o instalador NSIS, não para o MSI
|
||||
self.assertTrue(p["windows-x86_64"]["url"].endswith("x64-setup.exe"))
|
||||
self.assertTrue(p["windows-x86_64-msi"]["url"].endswith(".msi"))
|
||||
self.assertEqual(p["linux-x86_64"]["signature"], "assinatura-de-Guara_1.3.0_amd64.AppImage")
|
||||
|
||||
def test_missing_package_is_left_out(self):
|
||||
a = assets()
|
||||
del a["Guara_1.3.0_x64.app.tar.gz"]
|
||||
d = m.build("1.3.0", "", a, "2026-10-23T12:00:00Z")
|
||||
self.assertNotIn("darwin-x86_64", d["platforms"])
|
||||
self.assertIn("darwin-aarch64", d["platforms"])
|
||||
|
||||
def test_other_versions_do_not_leak(self):
|
||||
d = m.build("1.3.0", "", assets("1.2.0"), "2026-10-23T12:00:00Z")
|
||||
self.assertEqual(d["platforms"], {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user