release 1.3.2: update from Gitea, Linux-only CLI, desktop app for Linux and Windows
CI / Formatação e clippy (push) Successful in 2m5s
CI / Auditoria de dependências (push) Successful in 10s
CI / Compila o app desktop (Linux) (push) Successful in 1m14s
CI / Testes (Linux) (push) Successful in 4m36s

The updater reads latest.json from Gitea, built by scripts/make-latest-json.py
against the Gitea API (no gh CLI). Release workflow builds the Linux CLI and
the Linux+Windows desktop app, signs the updater artifacts and attaches
everything to the release.

Needs the repo secrets TAURI_SIGNING_PRIVATE_KEY and
TAURI_SIGNING_PRIVATE_KEY_PASSWORD, which I could not set (secret-store writes
are reserved for the user) - set them from ~/.tauri/guara-updater.key and
.pass before pushing a tag.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-30 13:41:21 -03:00
1 parent 8ba3151512
commit b47708126e
14 files changed
+319 -46

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Compila o app desktop (Tauri) e deixa os pacotes em dist-gui/.
#
# scripts/build-gui.sh linux # AppImage e .deb
# scripts/build-gui.sh windows # instalador NSIS, compilado a partir do Linux
# scripts/build-gui.sh windows-docker # o mesmo, num container, sem instalar nada
#
# O updater do Tauri assina cada pacote (createUpdaterArtifacts: true), então
# TAURI_SIGNING_PRIVATE_KEY e TAURI_SIGNING_PRIVATE_KEY_PASSWORD precisam estar
# no ambiente. Sem eles no ambiente, e com uma chave local em ~/.tauri (gerada
# por `tauri signer generate`), este script a carrega sozinho.
#
# O Windows usa cargo-xwin (SDK do Windows baixado na hora) e o NSIS; o Tauri
# documenta isso como compilação cruzada experimental. Precisa de: nsis, lld,
# llvm, clang e `cargo install cargo-xwin`. O .msi não sai daqui (o WiX é só do
# Windows).
set -euo pipefail
cd "$(dirname "$0")/.."
platform=${1:?uso: build-gui.sh <linux|windows|windows-docker>}
tauri() { npx --yes @tauri-apps/cli@2 "$@"; }
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ] && [ -f "$HOME/.tauri/guara-updater.key" ]; then
export TAURI_SIGNING_PRIVATE_KEY="$(cat "$HOME/.tauri/guara-updater.key")"
[ -f "$HOME/.tauri/guara-updater.pass" ] && export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat "$HOME/.tauri/guara-updater.pass")"
fi
: "${TAURI_SIGNING_PRIVATE_KEY:?defina TAURI_SIGNING_PRIVATE_KEY (ou gere a chave local com 'tauri signer generate')}"
if [ "$platform" = windows-docker ]; then
docker volume create guara-cargo >/dev/null
docker volume create guara-rustup >/dev/null
mkdir -p dist-gui
exec docker run --rm -v "$PWD":/src -v guara-cargo:/root/.cargo -v guara-rustup:/root/.rustup \
-w /src -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
-e TAURI_SIGNING_PRIVATE_KEY -e TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
ubuntu:24.04 bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl ca-certificates build-essential pkg-config nsis lld llvm clang nodejs npm unzip >/dev/null
[ -x /root/.cargo/bin/rustup ] || curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal >/dev/null
export PATH=/root/.cargo/bin:$PATH
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
scripts/build-gui.sh windows
chown -R "$HOST_UID:$HOST_GID" dist-gui apps/gui/src-tauri/target 2>/dev/null || true'
fi
mkdir -p dist-gui
cd apps/gui/src-tauri
[ -f icons/icon.ico ] || tauri icon icons/source.png -o icons
case "$platform" in
linux)
tauri build --bundles appimage,deb
bundle=target/release/bundle
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
cp "$bundle"/appimage/*.AppImage.sig ../../../dist-gui/ 2>/dev/null || true
;;
windows)
rustup target add x86_64-pc-windows-msvc >/dev/null
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
bundle=target/x86_64-pc-windows-msvc/release/bundle/nsis
cp "$bundle"/*-setup.exe ../../../dist-gui/
cp "$bundle"/*-setup.exe.sig ../../../dist-gui/ 2>/dev/null || true
;;
*) echo "plataforma desconhecida: $platform" >&2; exit 2 ;;
esac
ls -1 ../../../dist-gui
Regular → Executable
+26 -19
View File
@@ -1,29 +1,37 @@
#!/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.
assinaturas `.sig` já enviados à release no Gitea.
Roda no job de publicação, depois que todos os jobs de empacotamento terminaram. Montar o arquivo
Roda no job de publicação, depois que 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)
Uso: make-latest-json.py <tag> <notas.md> <saida.json>
Precisa de API (URL do repositório na API do Gitea) e TOKEN no ambiente.
"""
import json
import os
import subprocess
import sys
import tempfile
import urllib.request
from datetime import datetime, timezone
# (chaves de plataforma, padrão do nome do pacote). {v} = versão sem o "v".
# Sem macOS: não há runner para compilar essa plataforma.
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 api_get(path):
req = urllib.request.Request(f"{os.environ['API']}{path}", headers={"Authorization": f"token {os.environ['TOKEN']}"})
return json.load(urllib.request.urlopen(req))
def api_get_text(url):
req = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['TOKEN']}"})
return urllib.request.urlopen(req).read().decode()
def build(version, notes, assets, pub_date):
"""`assets` = {nome_do_pacote: (url, assinatura)}. Devolve o dicionário do latest.json.
@@ -42,20 +50,19 @@ def build(version, notes, assets, pub_date):
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"]}
rel = api_get(f"/releases/tags/{tag}")
by_name = {a["name"]: a 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())
for _, pattern in PLATFORMS:
name = pattern.format(v=version)
sig_name = name + ".sig"
if name in by_name and sig_name in by_name:
sig = api_get_text(by_name[sig_name]["browser_download_url"])
assets[name] = (by_name[name]["browser_download_url"], sig)
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"):
# Sem Linux e Windows o atualizador não funciona para ninguém: não publica pela metade.
for must in ("linux-x86_64", "windows-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:
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Compila o CLI para uma plataforma e empacota: guara-<tag>-<plataforma>.tar.gz
# com um .sha256 ao lado.
#
# scripts/package-cli.sh v1.3.2 linux-x86_64 x86_64-unknown-linux-gnu
#
# Plataformas: linux-x86_64 e linux-aarch64. O alvo diferente da máquina de
# build usa o linker cruzado correspondente (gcc-x86-64-linux-gnu). Escreve os
# arquivos na raiz do repositório e imprime os nomes.
set -euo pipefail
cd "$(dirname "$0")/.."
tag=${1:?uso: package-cli.sh <tag> <plataforma> <alvo-rust>}
platform=${2:?uso: package-cli.sh <tag> <plataforma> <alvo-rust>}
target=${3:?uso: package-cli.sh <tag> <plataforma> <alvo-rust>}
if [ "$target" = x86_64-unknown-linux-gnu ] && [ "$(uname -m)" != x86_64 ]; then
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
fi
rustup target add "$target" >/dev/null 2>&1 || true
cargo build --release --locked -p guara-cli --target "$target"
name="guara-${tag}-${platform}"
rm -rf "$name"; mkdir "$name"
cp "${CARGO_TARGET_DIR:-target}/${target}/release/guara" "$name/"
cp README.md LICENSE-MIT LICENSE-APACHE "$name/"
tar czf "$name.tar.gz" "$name"
asset="$name.tar.gz"
rm -rf "$name"
sha256sum "$asset" > "$asset.sha256"
echo "$asset"
echo "$asset.sha256"
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Anexa arquivos a uma release do Gitea, trocando os de mesmo nome (assim uma
# execução repetida do CI não falha por anexo duplicado).
#
# API=https://git.exemplo.com/api/v1/repos/dono/repo TOKEN=... \
# scripts/release-upload.sh v1.2.3 arquivo1 arquivo2
set -euo pipefail
: "${API:?defina API (URL do repositório na API do Gitea)}"
: "${TOKEN:?defina TOKEN}"
tag=${1:?uso: release-upload.sh <tag> <arquivo>...}
shift
auth=(-H "Authorization: token $TOKEN")
id=$(curl -fsS "${auth[@]}" "$API/releases/tags/$tag" | jq -r .id)
for file in "$@"; do
name=$(basename "$file")
for old in $(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id'); do
curl -fsS -X DELETE "${auth[@]}" "$API/releases/$id/assets/$old"
done
curl -fsS -X POST "${auth[@]}" -F "attachment=@$file" "$API/releases/$id/assets?name=$name" >/dev/null
echo "enviado: $name"
done
+11 -14
View File
@@ -8,37 +8,34 @@ spec = importlib.util.spec_from_file_location("m", os.path.join(os.path.dirname(
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
BASE = "https://github.com/x/y/releases/download/v1.3.0/"
BASE = "https://git.lucasrguerra.dev.br/lucasrguerra/guara/releases/download/v1.3.2/"
def assets(version="1.3.0"):
def assets(version="1.3.2"):
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")
d = m.build("1.3.2", "### Novidades\n", assets(), "2026-10-30T12:00:00Z")
self.assertEqual(d["version"], "1.3.2")
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"):
for k in ("linux-x86_64", "linux-x86_64-appimage", "windows-x86_64", "windows-x86_64-nsis"):
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")
self.assertEqual(p["linux-x86_64"]["signature"], "assinatura-de-Guara_1.3.2_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"])
del a["Guara_1.3.2_x64-setup.exe"]
d = m.build("1.3.2", "", a, "2026-10-30T12:00:00Z")
self.assertNotIn("windows-x86_64", d["platforms"])
self.assertIn("linux-x86_64", 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")
d = m.build("1.3.2", "", assets("1.2.0"), "2026-10-30T12:00:00Z")
self.assertEqual(d["platforms"], {})