release 1.3.2: update from Gitea, Linux-only CLI, desktop app for Linux and Windows
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:
1 parent
8ba3151512
commit
b47708126e
14 files changed
+319
-46
No files matched your search
Executable
+70
@@ -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
@@ -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:
|
||||
|
||||
Executable
+36
@@ -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"
|
||||
Executable
+24
@@ -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
|
||||
@@ -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"], {})
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user