# 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"