Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm check
- run: pnpm build
- name: The setup window's page builds too (spec 15.7)
run: pnpm exec vite build --mode setup
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Release

# Pushing a tag vX.Y.Z (matching the workspace version) builds the signed
# installer and opens a draft release; publishing it makes the in-app
# updater offer it (spec 15.5).
# installer and the setup window around it, and opens a draft release;
# publishing it makes the in-app updater offer it (spec 15.5, 15.7).
on:
push:
tags: ["v*"]
Expand Down Expand Up @@ -38,6 +38,9 @@ jobs:
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- name: Build the setup window around it
run: pnpm bundle:setup
working-directory: app
- name: Open a draft release
run: node app/scripts/release.mjs publish
env:
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pnpm bundle # instalador NSIS com o sidecar (spec 15.4), em target\release
pnpm bundle:setup # tela de instalação com esse NSIS dentro (spec 15.7), em target\release\bundle\setup
```

Release (spec 15.5): suba a versão em `[workspace.package]` do `Cargo.toml` (e no `app/package.json`), faça merge, crie a tag `vX.Y.Z` e dê push. O workflow `release.yml` abre um release rascunho com instalador, `.sig` e `latest.json`; o updater só o vê depois de publicado. A chave privada do updater fica no segredo `TAURI_SIGNING_PRIVATE_KEY`; perdê-la impede atualizar quem já instalou.
Release (spec 15.5): suba a versão em `[workspace.package]` do `Cargo.toml` (e no `app/package.json`), faça merge, crie a tag `vX.Y.Z` e dê push. O workflow `release.yml` abre um release rascunho com a tela de instalação (`-setup.exe`), o instalador do atualizador (`-update.exe`) com o `.sig` e o `latest.json`; o updater só o vê depois de publicado. `node app/scripts/release.mjs stage` monta esses arquivos localmente para conferir. A chave privada do updater fica no segredo `TAURI_SIGNING_PRIVATE_KEY`; perdê-la impede atualizar quem já instalou.

Em dev, use `$env:BOTLOFT_HOME = "$PWD\.dev\home"` (caminho absoluto) no daemon e no app, para não tocar na instalação real em `%LOCALAPPDATA%\Botloft`. O `config.toml` de dev fica dentro dessa pasta: o app lê a porta de lá.

Expand Down
55 changes: 38 additions & 17 deletions app/scripts/release.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
// Release steps for .github/workflows/release.yml (spec 15.5).
// Release steps for .github/workflows/release.yml (spec 15.5, 15.7).
//
// node app/scripts/release.mjs check the tag matches the workspace version
// node app/scripts/release.mjs publish draft GitHub release with the installer,
// its signature and latest.json
// node app/scripts/release.mjs stage the release files, with their final names,
// in target/release/bundle/release
// node app/scripts/release.mjs publish stage, then a draft GitHub release with them
//
// latest.json is the feed the in-app updater reads from the latest published
// release. A draft is invisible to it until the owner publishes the release.
// A release carries two installers: the setup window people download
// (`-setup.exe`, from `pnpm bundle:setup`) and the NSIS installer the in-app
// updater runs (`-update.exe`, from `pnpm bundle`, with its signature).
// latest.json, the feed the updater reads from the latest published release,
// points to the second. A draft is invisible to it until the owner publishes
// the release.

import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { root, workspaceVersion } from "./workspace.mjs";

Expand All @@ -23,33 +28,47 @@ function check() {
console.log(`releasing ${version}`);
}

function publish() {
/** Copies the release files under their final names and writes latest.json. */
function stage() {
const version = workspaceVersion();
const dir = join(root, "target", "release", "bundle", "nsis");
const installer = `Botloft_${version}_x64-setup.exe`;
const signature = readFileSync(join(dir, `${installer}.sig`), "utf8").trim();
const bundle = join(process.env.CARGO_TARGET_DIR ?? join(root, "target"), "release", "bundle");
const built = `Botloft_${version}_x64-setup.exe`;
const setup = `Botloft_${version}_x64-setup.exe`;
const update = `Botloft_${version}_x64-update.exe`;
const out = join(bundle, "release");
rmSync(out, { recursive: true, force: true });
mkdirSync(out, { recursive: true });

copyFileSync(join(bundle, "setup", built), join(out, setup));
copyFileSync(join(bundle, "nsis", built), join(out, update));
copyFileSync(join(bundle, "nsis", `${built}.sig`), join(out, `${update}.sig`));
const signature = readFileSync(join(out, `${update}.sig`), "utf8").trim();
const feed = {
version,
notes: "",
pub_date: new Date().toISOString(),
platforms: {
"windows-x86_64": {
signature,
url: `https://github.com/${repo}/releases/download/${tag}/${installer}`,
url: `https://github.com/${repo}/releases/download/${tag}/${update}`,
},
},
};
const latest = join(dir, "latest.json");
writeFileSync(latest, `${JSON.stringify(feed, null, 2)}\n`);
writeFileSync(join(out, "latest.json"), `${JSON.stringify(feed, null, 2)}\n`);
const files = [setup, update, `${update}.sig`, "latest.json"].map((name) => join(out, name));
console.log(`staged in ${out}:\n ${files.join("\n ")}`);
return { version, files };
}

function publish() {
const { version, files } = stage();
execFileSync(
"gh",
[
"release",
"create",
tag,
join(dir, installer),
join(dir, `${installer}.sig`),
latest,
...files,
"--draft",
"--title",
`Botloft ${version}`,
Expand All @@ -62,8 +81,10 @@ function publish() {
const step = process.argv[2];
if (step === "check") {
check();
} else if (step === "stage") {
stage();
} else if (step === "publish") {
publish();
} else {
throw new Error("usage: release.mjs check|publish");
throw new Error("usage: release.mjs check|stage|publish");
}
9 changes: 5 additions & 4 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ Identidade: o mascote do Botloft é uma chama com olhos. O ícone do app (`app/a
- Com versão nova, aparece "Update available" na barra de título. Um clique abre o diálogo: a versão, "What's new" (se o release tiver notas) e o aviso de que o Botloft fecha, instala e abre de novo, e de que os bots pausam por um instante e continuam de onde pararam. "Update now" baixa com progresso e roda o instalador; se falhar, o erro fica em "Details" e dá para tentar de novo.
- O feed aponta para o instalador NSIS do release, `Botloft_<versão>_x64-update.exe`, e não para a tela de instalação (15.7). O instalador roda como `/P /UPDATE /R`: passivo, sem perguntas, e reabre o app. O hook de desinstalação não faz nada com `/UPDATE` (15.4), o daemon segue rodando, e o app reaberto o atualiza porque ele ficou `outdated` (15.2).
- Os artefatos são assinados com a chave do updater do Tauri (`createUpdaterArtifacts`), e a chave pública fica no `tauri.conf.json`. `requireSignedVersion` exige que a assinatura traga a versão, para um feed adulterado não empurrar uma versão antiga de volta. O instalador não tem assinatura Authenticode, então o SmartScreen avisa na primeira execução.
- Release: a versão fica só no `[workspace.package]` do `Cargo.toml` (o `tauri.conf.json` não repete a versão e usa a do crate). Um push de tag `vX.Y.Z` roda `.github/workflows/release.yml`, que confere tag e versão, roda `pnpm bundle` com `TAURI_SIGNING_PRIVATE_KEY` (segredo do repositório) e abre um release **rascunho** com o instalador, o `.sig` e o `latest.json` (`app/scripts/release.mjs`). O updater só enxerga o release depois que o dono o publica.
- Release: a versão fica só no `[workspace.package]` do `Cargo.toml` (o `tauri.conf.json` não repete a versão e usa a do crate). Um push de tag `vX.Y.Z` roda `.github/workflows/release.yml`, que confere tag e versão, roda `pnpm bundle` com `TAURI_SIGNING_PRIVATE_KEY` (segredo do repositório), depois `pnpm bundle:setup` (15.7), e abre um release **rascunho** com a tela de instalação (`-setup.exe`), o instalador do atualizador (`-update.exe`) e o `.sig` dele, e o `latest.json` (`app/scripts/release.mjs`). O updater só enxerga o release depois que o dono o publica.

### 15.6 Idiomas

Expand Down Expand Up @@ -757,7 +757,8 @@ Identidade: o mascote do Botloft é uma chama com olhos. O ícone do app (`app/a
**Build e release.**

- `pnpm bundle:setup` (em `app/`, `scripts/setup.mjs`) roda `vite build --mode setup`, que gera só a `setup.html` em `app/dist-setup`, e `cargo build -p botloft-setup --release --features tauri/custom-protocol` com `BOTLOFT_SETUP_PAYLOAD` apontando para o instalador NSIS que o `pnpm bundle` acabou de gerar. O resultado vai para `target\release\bundle\setup\Botloft_<versão>_x64-setup.exe`. Com `-- --rehearse`, sai sem o instalador dentro. O setup não usa o CLI do Tauri, porque não precisa de bundle: basta o executável.
- O release (15.5) passa a ter a tela de instalação como `Botloft_<versão>_x64-setup.exe`, o nome que o README já indica, e o instalador NSIS como `Botloft_<versão>_x64-update.exe` com o `.sig`, que é para onde o `latest.json` aponta. O `release.mjs` copia os dois com esses nomes antes de publicar.
- O release (15.5) passa a ter a tela de instalação como `Botloft_<versão>_x64-setup.exe`, o nome que o README já indica, e o instalador NSIS como `Botloft_<versão>_x64-update.exe` com o `.sig`, que é para onde o `latest.json` aponta. O `release.mjs stage` copia os dois com esses nomes para `target\release\bundle\release\` e escreve o `latest.json`; o `publish` faz o `stage` e sobe o que ficou lá. O `stage` roda sozinho para conferir os arquivos sem criar release.
- O CI do app também roda `vite build --mode setup`, para a página do setup não quebrar só na hora do release.
- O setup não tem assinatura Authenticode, como o NSIS (15.5): o SmartScreen avisa ao abrir. O NSIS gravado na pasta temporária não tem a marca da internet e não passa de novo pelo SmartScreen.
- O executável fica maior, com a janela mais o NSIS inteiro: 15,4 MB na 0.5.0, contra 6,8 MB do NSIS sozinho.

Expand All @@ -766,7 +767,7 @@ Identidade: o mascote do Botloft é uma chama com olhos. O ícone do app (`app/a
- `/S` por cima de uma versão instalada: se desinstala a anterior (como o `/P` faz pela página de reinstalação) ou só sobrescreve, e se algum arquivo antigo fica. **Mesma versão** (0.5.0 sobre 0.5.0, 2026-09-29): nenhum processo de desinstalador apareceu (olhando a cada 100 ms); o NSIS só sobrescreveu os arquivos. Falta ver com uma versão mais antiga (I3).
- `/S` com o app aberto: se o NSIS fecha o app sozinho, pergunta ou falha. O setup fecha o app antes (verificado: "Instalar de novo" com o app aberto fechou o app, instalou e abriu de novo), mas o comportamento do NSIS sozinho precisa ficar anotado.
- `/S` com uma versão mais nova instalada: se sai com erro (`silentDowngrades`) e com qual código.
- O atualizador aceita o instalador com o nome novo (`-update.exe`) e o roda como NSIS.
- O atualizador aceita o instalador com o nome novo (`-update.exe`) e o roda como NSIS. **Na fonte** (`tauri-plugin-updater` 2.13, `extract_exe`): o tipo vem dos bytes baixados, e qualquer `.exe` é tratado como NSIS; o nome não conta. Falta ver a atualização de verdade da 0.5.0 para a 0.6.0.
- Com o manifesto `asInvoker`, o Windows não pede administrador para o setup. **Verificado** (Windows 11 25H2, 2026-09-29): o `Botloft_0.5.0_x64-setup.exe` abriu a janela direto, sem pedido do UAC.
- O identificador dos atalhos continua o mesmo, para os avisos saírem com o nome do Botloft.

Expand All @@ -779,7 +780,7 @@ Um teste que instala uma cópia em outra pasta e depois a desinstala apaga a ent
| Marco | Entrega | Pronto quando |
|---|---|---|
| **I1** Tela | crate `botloft-setup`, telas com `FakeSetup` e a prévia `?setup`, instalação pelo NSIS em silêncio, abrir o app ao terminar, modo de ensaio sem o instalador dentro | instalar pela tela nova numa máquina sem o Botloft e o app abrir; testes das telas e do `SetupHost` |
| **I2** Release | `pnpm bundle:setup`, `release.mjs` e `release.yml` com os dois arquivos, README | um release rascunho com a tela de instalação, o `-update.exe`, o `.sig` e o `latest.json` certo |
| **I2** Release | `pnpm bundle:setup`, `release.mjs` e `release.yml` com os dois arquivos; o README vai num PR próprio, junto com a 0.6.0 | um release rascunho com a tela de instalação, o `-update.exe`, o `.sig` e o `latest.json` certo |
| **I3** Acabamento | versão já instalada (mais antiga, igual, mais nova), app aberto, falha com o instalador clássico, sem WebView2 | cada caso testado à mão no Windows e anotado em "A verificar" |

Esta seção entra no PR do I1, junto com o código.
Expand Down
Loading