CI / Build & test (Node 20) (push) Waiting to run
CI / Build & test (Node 24) (push) Waiting to run
CI / Build & test (push) Blocked by required conditions
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
Security / Secret scan (gitleaks) (push) Waiting to run
Security / Dependency scan (OSV-Scanner) (push) Waiting to run
Security / IaC/config scan (Trivy) (push) Waiting to run
Security / License compliance scan (Trivy) (push) Waiting to run
Two defects, both mine, and the second hid the first. The cleanup built a regex and matched it against the jq line '<id> <name>', so the anchor in latest.yml could never match — it sits after the id. Only the .exe was removed, and each publish left another latest.yml behind. Gitea then served the older one, so the updater kept reporting the previous version as current: an update that exists and is never offered, with nothing logged anywhere. Filtering now happens inside jq, comparing the name directly. The guard meant to catch exactly that was missing its closing fi, so the step died on a syntax error before reaching it. Verified with bash -n this time, which is what should have happened before it ever ran on a runner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
222 lines
9.0 KiB
YAML
222 lines
9.0 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.
|
|
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
|
|
|
|
# Filtro dentro do jq, comparando o nome direto. A versão anterior
|
|
# montava um regex e o casava contra "<id> <nome>", onde o `^` de
|
|
# `^latest\.yml$` ancorava no início da linha — depois do id — e nunca
|
|
# podia casar. O laço então só removia os .exe e deixava um latest.yml
|
|
# duplicado, que é o arquivo que decide se há atualização.
|
|
OLD=$(body "$R" | jq -r --arg os "${RUNNER_OS:-}" '
|
|
.[]
|
|
| select(
|
|
if $os == "Windows"
|
|
then (.name | endswith(".exe")) or .name == "latest.yml"
|
|
else (.name | endswith(".AppImage")) or (.name | endswith(".deb")) or .name == "latest-linux.yml"
|
|
end
|
|
)
|
|
| "\(.id) \(.name)"')
|
|
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"
|
|
fi
|