OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (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
The system balloon carries the OS notification sound, which does not belong to this app, and it only fired while the window was out of focus — with the app focused nothing appeared at all. Notifications now surface inside the window, carry the same synthesised timbre as the rest of the app's sounds, and clicking one opens the channel. The native balloon is kept for when the window is not visible, since an in-app card nobody can see is no notification, but it is now silent: the app plays its own effect instead. A focused window is notified only about other channels — announcing the conversation someone is already reading is noise. Also fixes the Gitea publish cleanup, which silently deleted nothing: it interpolated an Actions expression inside a bash , and when the pattern did not match, the loop passed over every asset. The release ended with two latest.yml files and the updater served the older one, reporting 1.1.0 as current — an update that exists but is never offered, with no error anywhere. The filter is plain bash now, logs what it found, and the job fails if more than one latest.yml survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
214 lines
8.4 KiB
YAML
214 lines
8.4 KiB
YAML
# Compila no GitHub, publica no Gitea.
|
|
#
|
|
# O GitHub entra só como máquina de build — é dele que vêm os runners Windows
|
|
# de que o módulo nativo de áudio precisa. A distribuição fica no Gitea, que
|
|
# serve os arquivos a qualquer um: assim o electron-updater não precisa de
|
|
# credencial embutida no app, o que aconteceria com um repositório privado no
|
|
# GitHub.
|
|
name: Publicar no Gitea
|
|
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Tag a publicar (ex. v1.1.0)
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: windows-2022
|
|
args: --win --x64
|
|
- os: ubuntu-latest
|
|
args: --linux --x64
|
|
runs-on: ${{ matrix.os }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
|
|
|
- name: Install Linux build dependencies
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y \
|
|
libx11-dev libxtst-dev libxt-dev \
|
|
libxkbcommon-dev libxkbcommon-x11-dev libxkbfile-dev \
|
|
libxrandr-dev libxinerama-dev libx11-xcb-dev \
|
|
libpipewire-0.3-dev libpulse-dev
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
|
with:
|
|
version: 10.34.3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
|
with:
|
|
node-version: 20
|
|
cache: pnpm
|
|
|
|
- name: Cache Electron binaries
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cache/electron
|
|
~/.cache/electron-builder
|
|
~\AppData\Local\electron\Cache
|
|
~\AppData\Local\electron-builder\Cache
|
|
key: electron-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
restore-keys: electron-cache-${{ runner.os }}-
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Build shared package
|
|
run: pnpm --filter @backspace/shared build
|
|
|
|
# O postinstall termina em `|| console.warn` para quem não tem ferramentas
|
|
# de build. No CI isso esconde falha: o instalador sairia sem captura de
|
|
# áudio do sistema e ninguém saberia. Verifica-se o resultado.
|
|
- name: Verify native audio module compiled
|
|
if: runner.os == 'Windows'
|
|
shell: bash
|
|
run: |
|
|
found=$(find node_modules/.pnpm -path '*electron-native-screenshare*' -name '*.node' | head -5)
|
|
[ -n "$found" ] || { echo "::error::sem .node compilado — instalador sairia sem áudio do sistema"; exit 1; }
|
|
echo "$found"
|
|
|
|
- name: Compile desktop TypeScript
|
|
working-directory: packages/desktop
|
|
run: pnpm exec tsc
|
|
|
|
# --publish never: o electron-builder não sabe enviar para o Gitea. Ele
|
|
# gera os instaladores e o latest.yml (o índice que o app consulta), e o
|
|
# passo seguinte faz o upload.
|
|
- name: Build installers
|
|
working-directory: packages/desktop
|
|
run: pnpm exec electron-builder ${{ matrix.args }} --publish never
|
|
env:
|
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
|
|
|
- name: Upload to Gitea release
|
|
shell: bash
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
GITEA_API: https://git.resenha.website/api/v1/repos/devsyncwrld/backspace
|
|
TAG: latest
|
|
run: |
|
|
set -uo pipefail
|
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
|
echo "::error::segredo GITEA_TOKEN não configurado"
|
|
exit 1
|
|
fi
|
|
|
|
# Sem -f e imprimindo o corpo: a versão anterior usava `curl -sf`, que
|
|
# engole a resposta de erro, então uma falha aqui só aparecia como um
|
|
# JSONDecodeError sem dizer o motivo.
|
|
api() {
|
|
local method=$1 path=$2; shift 2
|
|
curl -s -w '\n%{http_code}' -X "$method" \
|
|
-H "Authorization: token $GITEA_TOKEN" "$GITEA_API$path" "$@"
|
|
}
|
|
body() { sed '$d' <<<"$1"; }
|
|
code() { tail -n1 <<<"$1"; }
|
|
# jq em vez de python embutido: um heredoc multilinha dentro de um
|
|
# bloco literal de YAML encerra o bloco na primeira linha sem recuo.
|
|
json_id() { jq -r '.id // empty'; }
|
|
|
|
find_release() {
|
|
local r; r=$(api GET "/releases/tags/$TAG")
|
|
[ "$(code "$r")" = "200" ] && body "$r" | json_id || echo ""
|
|
}
|
|
|
|
# O corpo do POST fica em ASCII de proposito: o shell do runner
|
|
# Windows corrompe UTF-8 na requisicao e o Gitea recusa com
|
|
# "invalid UTF-8 within /name" (HTTP 422).
|
|
ID=$(find_release)
|
|
if [ -z "$ID" ]; then
|
|
R=$(api POST "/releases" -H 'Content-Type: application/json' \
|
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"Latest build\",\"target_commitish\":\"main\",\"body\":\"Installers published automatically by CI.\"}")
|
|
if [ "$(code "$R")" = "201" ]; then
|
|
ID=$(body "$R" | json_id)
|
|
echo "release criada: $ID"
|
|
else
|
|
echo "criação retornou HTTP $(code "$R"): $(body "$R")"
|
|
# O outro job da matriz pode tê-la criado no mesmo instante.
|
|
ID=$(find_release)
|
|
[ -n "$ID" ] || { echo "::error::não foi possível obter nem criar a release"; exit 1; }
|
|
echo "release encontrada após corrida: $ID"
|
|
fi
|
|
else
|
|
echo "release existente: $ID"
|
|
fi
|
|
|
|
# Remove só os anexos que esta plataforma vai repor, para os dois jobs
|
|
# não apagarem o trabalho um do outro.
|
|
#
|
|
# Padrão montado em bash puro: a versão anterior interpolava uma
|
|
# expressão do Actions dentro de um `case`, e quando ela não casou o
|
|
# laço passou em silêncio — a release ficou com dois latest.yml e o
|
|
# updater serviu o antigo, dizendo que a versão nova não existia.
|
|
if [ "${RUNNER_OS:-}" = "Windows" ]; then
|
|
MINE='\.exe$|^latest\.yml$'
|
|
else
|
|
MINE='\.AppImage$|\.deb$|^latest-linux\.yml$'
|
|
fi
|
|
|
|
R=$(api GET "/releases/$ID/assets")
|
|
if [ "$(code "$R")" != "200" ]; then
|
|
echo "::error::não foi possível listar os anexos — HTTP $(code "$R"): $(body "$R")"
|
|
exit 1
|
|
fi
|
|
|
|
OLD=$(body "$R" | jq -r '.[] | "\(.id) \(.name)"' | grep -E " .*($MINE)" || true)
|
|
echo "anexos desta plataforma já na release: $(printf '%s' "$OLD" | grep -c . || true)"
|
|
if [ -n "$OLD" ]; then
|
|
printf '%s\n' "$OLD" | while read -r aid aname; do
|
|
[ -n "$aid" ] || continue
|
|
echo "removendo anexo antigo: $aname"
|
|
D=$(api DELETE "/releases/$ID/assets/$aid")
|
|
[ "$(code "$D")" = "204" ] || echo "::warning::falha ao remover $aname — HTTP $(code "$D")"
|
|
done
|
|
fi
|
|
|
|
shopt -s nullglob
|
|
sent=0
|
|
for f in packages/desktop/dist-electron/*.exe \
|
|
packages/desktop/dist-electron/*.AppImage \
|
|
packages/desktop/dist-electron/*.deb \
|
|
packages/desktop/dist-electron/latest*.yml; do
|
|
name=$(basename "$f")
|
|
R=$(api POST "/releases/$ID/assets?name=$name" -F "attachment=@$f")
|
|
if [ "$(code "$R")" != "201" ]; then
|
|
echo "::error::falha ao enviar $name — HTTP $(code "$R"): $(body "$R")"
|
|
exit 1
|
|
fi
|
|
echo "enviado: $name"
|
|
sent=$((sent+1))
|
|
done
|
|
[ "$sent" -gt 0 ] || { echo "::error::o build não produziu instaladores"; exit 1; }
|
|
echo "$sent arquivo(s) publicados"
|
|
|
|
# O updater busca latest.yml pelo nome. Duas cópias com o mesmo nome
|
|
# fazem o Gitea servir a mais antiga, e a atualização deixa de ser
|
|
# oferecida — sem erro em lugar nenhum. Falha aqui em vez de publicar
|
|
# uma release que parece boa e não atualiza.
|
|
if [ "${RUNNER_OS:-}" = "Windows" ]; then
|
|
R=$(api GET "/releases/$ID/assets")
|
|
DUP=$(body "$R" | jq -r '[.[] | select(.name == "latest.yml")] | length')
|
|
if [ "$DUP" != "1" ]; then
|
|
echo "::error::a release tem $DUP cópias de latest.yml — o updater serviria a errada"
|
|
exit 1
|
|
fi
|
|
echo "latest.yml: 1 cópia, como esperado"
|