ci: build on GitHub, publish updates from Gitea
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

GitHub stays the build machine — it has the Windows runners the native audio
module needs — but the update feed moves to this fork's own Gitea.

The GitHub repository is private, and electron-updater against a private GitHub
repo needs a token inside the shipped app, which is a leaked token. Gitea
serves release assets to anyone, so the installer carries no credential. The
flow was verified end to end against the live instance before writing this:
create release, upload asset, download anonymously.

The release tag is fixed at 'latest' because electron-updater fetches
latest.yml before it knows which version exists, so the URL cannot carry a
version; CI replaces that release's assets each publish. electron-builder runs
with --publish never since it cannot upload to Gitea, but still emits the
latest.yml the updater reads.

Needs a GITEA_TOKEN secret on the GitHub repository.
This commit is contained in:
2026-09-01 15:49:25 -03:00
parent 5b84843217
commit 9f7723d104
2 changed files with 165 additions and 5 deletions
+152
View File
@@ -0,0 +1,152 @@
# 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
- 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 -euo pipefail
if [ -z "${GITEA_TOKEN:-}" ]; then
echo "::error::segredo GITEA_TOKEN não configurado no repositório"
exit 1
fi
# A release `latest` é recriada a cada publicação: o electron-updater
# busca latest.yml antes de saber qual versão existe, então a URL não
# pode conter número de versão.
ID=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_API/releases/tags/$TAG" | python3 -c \
"import json,sys;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$ID" ]; then
echo "release existente ($ID) — removendo anexos desta plataforma"
curl -sf -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/releases/$ID/assets" \
| python3 -c "import json,sys;[print(a['id'],a['name']) for a in json.load(sys.stdin)]" \
| while read -r aid aname; do
case "$aname" in
*.exe|*.AppImage|*.deb|latest*.yml)
curl -s -o /dev/null -X DELETE -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_API/releases/$ID/assets/$aid" ;;
esac
done
else
ID=$(curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$TAG\",\"name\":\"Última versão\",\"body\":\"Instaladores publicados automaticamente pelo CI.\"}" \
"$GITEA_API/releases" | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")
echo "release criada: $ID"
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")
echo "enviando $name"
curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$f" "$GITEA_API/releases/$ID/assets?name=$name" > /dev/null
sent=$((sent+1))
done
[ "$sent" -gt 0 ] || { echo "::error::nada para enviar — o build não produziu instaladores"; exit 1; }
echo "$sent arquivo(s) publicados"
+13 -5
View File
@@ -22,11 +22,19 @@ asarUnpack:
npmRebuild: false
afterPack: ./scripts/afterPack.js
publish:
# Fork: releases (and therefore the electron-updater feed) come from this
# repository, not upstream's.
- provider: github
owner: syncwrld
repo: resenhacord
# Updates are served from this fork's own Gitea, not from GitHub.
#
# GitHub is only the build machine — it has the Windows runners the native
# audio module needs. Its repository is private, and electron-updater against
# a private GitHub repo would need a token shipped inside the app, which is a
# leaked token. Gitea serves release assets to anyone, so no credential ends
# up in the installer.
#
# The tag is fixed at `latest` on purpose: electron-updater fetches
# latest.yml before it knows which version exists, so the URL cannot contain
# a version. CI replaces that release's assets on every publish.
- provider: generic
url: https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/
protocols:
- name: Backspace
schemes: